diff --git a/doc/rfc/submitqueue/speculation.md b/doc/rfc/submitqueue/speculation.md index 6f3d460e..a0a744d9 100644 --- a/doc/rfc/submitqueue/speculation.md +++ b/doc/rfc/submitqueue/speculation.md @@ -1,10 +1,10 @@ # Speculation -A merge queue that verifies one change at a time is limited by its slowest build. Speculation removes that limit: it builds a batch early, against a guess at how the conflicting batches ahead of it will resolve, so a valid build is usually ready by the time they do. +A merge queue that verifies one change at a time is limited by its slowest build. Speculation removes that limit: it builds a batch early, against an assumption about how the conflicting batches ahead of it will resolve, so a valid build is usually ready by the time they do. -Work enters SubmitQueue as **batches** — changes verified and merged together. Two batches **conflict** when they touch the same code, which makes the earlier one a **dependency** of the later. A **path** is one guess at how a batch's dependencies resolve, and the batch it builds is the path's **head**. +Work enters SubmitQueue as **batches** — changes verified and merged together. Two batches **conflict** when they touch the same code, which makes the earlier one a **dependency** of the later. A **path** is one set of assumptions about how a batch's dependencies resolve, and the batch it builds is the path's **head**. -On every queue update the **speculate controller** reruns from scratch: it reads the current state, applies the incoming signals, asks a pluggable **Speculator** which paths are worth building within the CI budget, and persists only those. Everything else is recomputed next time, never stored. Merging stays strict: a batch lands only after its dependencies resolve and a matching build has passed. +On every queue update the **speculate controller** reruns from scratch: it reads the current state, applies the incoming signals, asks a pluggable **Speculator** which paths are worth building within the CI budget, and persists only those. Everything else is recomputed next time, never stored. Merging stays strict: a batch merges only after its dependencies resolve and a matching build has passed. ## The speculation run @@ -25,7 +25,7 @@ The speculate controller runs whenever the queue changes — after a new batch, │ │ 1 read in-flight batches, finalized dependencies they still reference, │ and all materialized path sets — once; never re-read mid-run - │ 2 apply signals — refute paths whose bet a resolved dependency broke, + │ 2 apply signals — refute paths whose assumption a resolved dependency broke, │ finalize verdicts (below) │ 3 ask Speculator.Speculate(batches, path sets) ◀── the only extension call │ → paths to build, paths to preempt @@ -46,7 +46,7 @@ The speculate controller runs whenever the queue changes — after a new batch, Every run recomputes the whole queue from that single read — the batch ID on the trigger only wakes it. -Each path-status transition is written by the controller that observes it — nothing is polled or derived. The speculate run records a path *pending* when it funds it, and sets the *cancelling* intent when it refutes a path (a resolved dependency broke its bet — an included dependency failed, or an excluded one landed) or when a batch is cancelled. The build controller flips *pending → building* when it actually starts CI, so a backed-up build queue leaves the path *pending*. Each run idempotently re-sends the build dispatch for a funded pending path, keyed by (path ID, attempt), until the build controller records it *building* or terminal. The buildsignal consumer records the terminal status — *passed*, *failed*, or *cancelled* — when the build reaches it. The path stores no build ID; the execution record holds it, keyed by (path ID, attempt). +Each path-status transition is written by the controller that observes it — nothing is polled or derived. The speculate run records a path *pending* when it funds it, and sets the *cancelling* intent when it refutes a path (a resolved dependency broke its assumption — one assumed to succeed failed, or one assumed to fail succeeded) or when a batch is cancelled. The build controller flips *pending → building* when it actually starts CI, so a backed-up build queue leaves the path *pending*. Each run idempotently re-sends the build dispatch for a funded pending path, keyed by (path ID, attempt), until the build controller records it *building* or terminal. The buildsignal consumer records the terminal status — *passed*, *failed*, or *cancelled* — when the build reaches it. The path stores no build ID; the execution record holds it, keyed by (path ID, attempt). Every write is a compare-and-swap: a writer that loses re-reads on a later run. No run depends on an earlier one, so duplicated or reordered dirty signals are harmless. Any later queue update also reconciles earlier committed state because every run recomputes the whole queue. @@ -54,17 +54,17 @@ Every write is a compare-and-swap: a writer that loses re-reads on a later run. Verdicts are controller-owned facts: the Speculator can neither compute nor veto them. -- **Merge (strict).** Each path carries a bet on every dependency — *included* (built on top of), *excluded* (built without), or *dropped* (ignored). Once a path's build has passed and every *included* dependency has merged, the speculate controller moves the head to Merging and hands it to Runway — it waits only on the dependencies it was built on top of, not the head's full dependency list. If that hand-off is lost, the next run re-sends it. The mergesignal controller records Runway's terminal result: success marks the head Succeeded, while failure marks it Failed. The result publishes a single dirty signal — no per-dependent fan-out — and the next run refutes paths whose bet disagrees with the result: excluded bets after success, included bets after failure. The hand-off is idempotent, so Runway reports success without another merge when the change is already present. Down a chain, each head waits for its included predecessors, so a chain lands one at a time. +- **Merge (strict).** Each path carries an assumption about every dependency — *succeeds* (built on top of), *fails* (built without), or *ignored*. Once a path's build has passed and every dependency it assumes *succeeds* has merged, the speculate controller moves the head to Merging and hands it to Runway — it waits only on the dependencies it was built on top of, not the head's full dependency list. If that hand-off is lost, the next run re-sends it. The same run sets the head's remaining in-flight paths *cancelling*: once one path has passed the others cannot help, and they hold CI slots until they stop. The mergesignal controller records Runway's terminal result: success marks the head Succeeded, while failure marks it Failed. The result publishes a single dirty signal — no per-dependent fan-out — and the next run refutes paths whose assumption disagrees with the result: *fails* assumptions after success, *succeeds* assumptions after failure. The hand-off is idempotent, so Runway reports success without another merge when the change is already present. Down a chain, each head waits for the predecessors it assumes succeed, so a chain merges one at a time. - **Failure (no viable path).** A batch fails when every possible future has a failed build — no path can pass, so it can never merge. - **Cancel.** A cancelled batch is driven terminal: its in-flight paths are set *cancelling*, then the batch is marked Cancelled once they stop (see Cancellation). ### Conflict relaxation -Conflict analysis is conservative — it flags any *possible* conflict — so heads carry dependencies that rarely matter and over-serialize. Relaxation lets the Speculator **drop** the weakest: the path tags that dependency *dropped* and ignores it — it neither gates the merge nor refutes the path if it lands. Which to drop is a per-run Speculator policy. +Conflict analysis is conservative — it flags any *possible* conflict — so heads carry dependencies that rarely matter and over-serialize. Relaxation lets the Speculator **ignore** the weakest: the path marks that dependency *ignored*, and its outcome neither gates the merge nor refutes the path. Which to ignore is a per-run Speculator policy. -The drop lives on the path, so the path stays self-describing: finalization needs no external relaxed set (relaxing is what shrinks the space of guesses a head's paths range over). +That marking lives on the path, so the path stays self-describing: finalization needs no external relaxed set (relaxing is what shrinks the space of outcomes a head's paths range over). -Example: `H` conflicts with `B1` and weak `B2`. Drop `B2`, and `H` merges once `B1` lands and its build passes — even if `B2` later lands. Without it, `H` waits on both. +Example: `H` conflicts with `B1` and weak `B2`. Ignore `B2`, and `H` merges once `B1` merges and its build passes — even if `B2` later merges. Without it, `H` waits on both. ### Bypass large diff @@ -74,14 +74,14 @@ The default Speculator covers the whole space only when doing so is cheap enough ### Cancellation -Cancellation is best-effort: a batch marked *cancelling* may still land if a merge wins the race, so terminal states prevail. A cancel sets the intent; a later run drives it terminal. +Cancellation is best-effort: a batch marked *cancelling* may still merge if a merge wins the race, so terminal states prevail. A cancel sets the intent; a later run drives it terminal. Two kinds of cancel, split by owner: - **Preemption (extension).** The Speculator may propose `Cancel` on an in-flight path whose head is Speculating to free budget for a better candidate — its only cancel power. The controller validates it (never a passed path) and enacts it. -- **Correctness cancels (controller).** Refutation — a resolved dependency breaks a path's bet, so the controller cancels that path — and batch cancellation (below). The Speculator is not consulted. +- **Correctness cancels (controller).** Refutation — a resolved dependency breaks a path's assumption, so the controller cancels that path — and batch cancellation (below). The Speculator is not consulted. -Batch cancellation (user-initiated) marks the batch *cancelling*: a non-terminal intent that halts new work. The speculate run drives it terminal — it sets its in-flight paths *cancelling*, marks the batch *cancelled* once they stop, recomputes dependent paths against that outcome (those that bet it out keep building; those that bet it in are refuted), and concludes the contained requests. +Batch cancellation (user-initiated) marks the batch *cancelling*: a non-terminal intent that halts new work. The speculate run drives it terminal — it sets its in-flight paths *cancelling*, marks the batch *cancelled* once they stop, recomputes dependent paths against that outcome (those that assume it fails keep building; those that assume it succeeds are refuted), and concludes the contained requests. Cancelling a path sends a cancel (path ID, attempt) to the build controller, which cancels the CI build; that build holds its slot until the cancel reaches terminal. @@ -101,116 +101,15 @@ The one extension. It decides *which paths to build and which running ones to ca The default Speculator is composed from two swappable interfaces — a **Generator** and an **Allocator** — so scoring and preemption policy can vary independently. They are composition points inside the default implementation, not controller-facing extensions: the controller depends only on the Speculator contract, and an alternate Speculator need not use or expose this split. The default opens the Generator's candidate stream over the batches, then hands that stream and the path sets to the Allocator. -- **Generator** — yields the queue's candidate paths as one iterator, best-first across heads in `BatchStateSpeculating`. *Contract:* every candidate has a Speculating head and is coherent; none repeats or contradicts a resolved fact. Ranking is implementation-owned: the Generator may compute it directly, call an injected scorer extension, or use other injected data. It carries the resulting ranking score only within the run. -- **Allocator** — spends the build budget (the queue's cap on concurrent builds) over the iterator. *Contract:* it pulls in order until the budget fills and matches candidates to existing paths by ID, so a pending or building path remains funded rather than starting a new attempt, and a candidate whose path is already terminal in the path sets is skipped rather than rebuilt; pending dispatches are replayed by the controller as described above. Pending, building, and cancelling paths charge the budget (a cancelling build holds CI until terminal), while terminal ones charge none. Cancellation is best-effort, so the Allocator does not spend capacity it merely expects a cancel to release and risk exceeding the hard CI cap. *Default:* the sticky policy fills only free slots and leaves in-flight builds running; a preempting policy cancels in-flight paths below the funded set. Budget is the only rationing lever — there is no ranking-score floor. A build cancelled to make room still charges budget until its cancel reaches terminal and publishes dirty, so the next run funds the released slot — the queue converges over successive ticks rather than oversubscribing in a single pass. +- **Generator** — yields the queue's candidate paths as one iterator across heads in `BatchStateSpeculating`. *Contract:* every candidate has a Speculating head and is coherent; none repeats or contradicts a resolved fact. Ranking is implementation-defined — the Generator may compute it directly, call an injected scorer extension, or use other injected data — and the score it carries is meaningful only within the run. The Allocator consumes the iterator in the order the Generator yields it and does not interpret the score. *Default:* `bestfirst` ranks best-first by the probability that a path's assumptions all hold. +- **Allocator** — spends the build budget (the queue's cap on concurrent builds) over the iterator. *Contract:* it pulls in order until the budget fills and matches candidates to existing paths by ID, so a pending or building path keeps the slot it already holds rather than starting a second attempt, and a candidate whose path is already terminal in the path sets is skipped rather than rebuilt; pending dispatches are replayed by the controller as described above. Pending, building, and cancelling paths charge the budget (a cancelling build holds CI until terminal), while terminal ones charge none. Cancellation is best-effort, so the Allocator does not spend capacity it merely expects a cancel to release and risk exceeding the hard CI cap. *Default:* the sticky policy fills only free slots and leaves in-flight builds running; a preempting policy cancels in-flight paths below the funded set. Budget is the only rationing lever — there is no ranking-score floor. A build cancelled to make room still charges budget until its cancel reaches terminal and publishes dirty, so the next run funds the released slot — the queue converges over successive ticks rather than oversubscribing in a single pass. ### Extension APIs -```go -// SpeculationPathEntry is the stored record of one chosen path, keyed by a hash -// of its content. It holds no build reference (that is the execution record's) -// and no ranking score (a ranking score is meaningful only within a single run). -type SpeculationPathEntry struct { - ID string // primary key; hash of the path's content (head batch ID + its bets) - Path SpeculationPath // head batch ID + one bet per dependency, in queue order - Status SpeculationPathStatus // pending | building | passed | failed | cancelling | cancelled (current attempt) - Attempt int // build attempt number, >= 1; builds and messages key on (ID, Attempt) - Version int // for compare-and-swap writes - CreatedAt int64 // ms - UpdatedAt int64 // ms -} - -// SpeculationPath identifies a head batch and its ordered dependency bets; every -// dependency appears exactly once, so the logical path is self-describing. -type SpeculationPath struct { - Head string // ID of the batch being built - Bets []DependencyBet // one bet per dependency of Head, in queue order -} - -// DependencyBet is the path's bet on one dependency. -type DependencyBet struct { - Batch string // dependency batch ID - Bet DependencyBetType // included | excluded | dropped -} - -// DependencyBetType is how a path treats one dependency. -type DependencyBetType string - -const ( - BetUnknown DependencyBetType = "" // zero-value sentinel; never valid - BetIncluded DependencyBetType = "included" // bet it lands; head built on top of it, refuted if it fails - BetExcluded DependencyBetType = "excluded" // bet it does not land; refuted if it lands - BetDropped DependencyBetType = "dropped" // dropped by relaxation; landing or failing never affects the path -) - -// SpeculationPathSet is one head's chosen paths — live and recently finished — -// under a single version. Finished entries linger briefly so a re-run cannot -// collide with an old build; live heads are listed via the batch store's -// by-state query. Every logical path is self-describing, but a store may encode -// the common head and ordered dependency IDs once per set and store each path's -// bets positionally — two bits per dependency, or a base-3 code that stays a -// small integer. -type SpeculationPathSet struct { - Head string // primary key; ID of the head batch - Paths []SpeculationPathEntry // the head's chosen paths - Version int // for compare-and-swap writes -} -``` +Signatures live in code and are not copied here, so they cannot drift. This section says what each contract is for and where to read it. -```go -// Speculator selects build and preemption actions from one queue snapshot. -// batches contains every in-flight batch plus finalized batches still referenced -// by an in-flight batch. pathSets contains every materialized path for those -// batches, terminal or not. Returned actions must target Speculating heads; -// finalization remains controller-owned. -type Speculator interface { - Speculate(ctx context.Context, batches []entity.Batch, pathSets []entity.SpeculationPathSet) ([]Speculation, error) -} - -// Speculation is one proposed action on one path; a kept path has no entry. -type Speculation struct { - Path entity.SpeculationPath // the path acted on; its ID hashes head batch ID + its bets - Action PathAction // build | cancel -} - -// The only two actions the Speculator may propose. No Merge or Fail — -// verdicts are the controller's. -type PathAction string - -const ( - PathActionUnknown PathAction = "" // zero-value sentinel; never valid, rejected by the controller's check step - PathActionBuild PathAction = "build" // start (or resurrect) a build for the path - PathActionCancel PathAction = "cancel" // preempt this in-flight path (refutation cancels are the controller's) -) -``` +**Entities** — [`submitqueue/entity/speculation.go`](../../../submitqueue/entity/speculation.go). A `SpeculationPath` is a head batch plus one `PathDependency` per dependency in queue order, each carrying a `DependencyAssumption`: *succeeds*, *fails*, or *ignored*. A `SpeculationPathEntry` is the stored record of one chosen path, keyed by a hash of its content, plus its status and attempt number; it holds no build reference — the execution record has that, keyed by (path ID, attempt) — and no ranking score, which means nothing outside the run that produced it. A `SpeculationPathSet` is one head's chosen paths, live and recently finished, under a single version for compare-and-swap. Every logical path is self-describing, but a store may encode the common head and ordered dependency IDs once per set and keep each path's assumptions positionally — two bits per dependency, or a base-3 code that stays a small integer. -```go -// Generator — the queue's candidate stream. The consumer pulls candidates -// lazily; the producer computes only what is pulled. -type Generator interface { - // Open starts the queue's candidate stream, best-first across Speculating heads. - Open(ctx context.Context, batches []entity.Batch) (PathIterator, error) -} - -type PathIterator interface { - // Next yields the next-best candidate across Speculating heads; ok = false means the - // queue's coherent space is exhausted. Candidates descend in - // ranking score, never repeat, and never contradict a resolved fact. - Next(ctx context.Context) (c CandidatePath, ok bool, err error) -} - -// CandidatePath is a path plus its transient ranking score for this run. -type CandidatePath struct { - Path entity.SpeculationPath // head batch ID + one bet per dependency - RankingScore float64 // transient ordering score; never stored because rankings go stale across runs -} -``` +**Speculator** — [`submitqueue/extension/speculation/speculator`](../../../submitqueue/extension/speculation/speculator/README.md). `Speculate` takes one queue snapshot (the batches and their path sets) and returns the build and cancel actions it proposes; a path it wants left alone has no entry. Actions must target Speculating heads. Verdicts stay controller-owned, so there is no merge or fail action. -```go -// Allocator — spends the build budget over the iterator, matching in-flight -// paths to the funded set by ID and skipping candidates whose path is already -// terminal in the path sets. Budget and clock are injected at construction. -type Allocator interface { - Allocate(ctx context.Context, pathSets []entity.SpeculationPathSet, iter PathIterator) ([]Speculation, error) -} -``` +**Generator and Allocator** — [`generator`](../../../submitqueue/extension/speculation/generator/README.md) and [`allocator`](../../../submitqueue/extension/speculation/allocator/README.md), the two composition points inside the default Speculator. The Generator opens a pull-based stream of candidate paths over the batches; the Allocator spends the build budget over that stream, reconciling it against the path sets. Both abort on a cancelled context. diff --git a/submitqueue/entity/speculation.go b/submitqueue/entity/speculation.go index 6ed3f558..bb4cb4f4 100644 --- a/submitqueue/entity/speculation.go +++ b/submitqueue/entity/speculation.go @@ -20,57 +20,60 @@ import ( "strings" ) -// DependencyBetType is how a path treats one of its head's dependencies. -type DependencyBetType string +// DependencyAssumption is what a path assumes about one dependency's outcome. +type DependencyAssumption string const ( - // BetUnknown is the zero-value sentinel; it is never a valid bet. - BetUnknown DependencyBetType = "" - // BetIncluded bets the dependency lands: the head is built on top of it, and - // the path is invalidated if the dependency ultimately fails. - BetIncluded DependencyBetType = "included" - // BetExcluded bets the dependency does not land: the head is built without it, - // and the path is invalidated if the dependency ultimately lands. - BetExcluded DependencyBetType = "excluded" - // BetDropped marks a dependency ignored by conflict relaxation: whether it - // lands or fails never affects the path — it neither gates the merge nor - // invalidates the path. - BetDropped DependencyBetType = "dropped" + // DependencyAssumptionUnknown is the zero-value sentinel; it is never a + // valid assumption. + DependencyAssumptionUnknown DependencyAssumption = "" + // DependencyAssumptionSucceeds assumes the dependency succeeds: the head is + // built on top of it, and the path is refuted if it does not. + DependencyAssumptionSucceeds DependencyAssumption = "succeeds" + // DependencyAssumptionFails assumes the dependency does not succeed — + // whether it fails or is cancelled. The head is built without it, and the + // path is refuted if it succeeds after all. + DependencyAssumptionFails DependencyAssumption = "fails" + // DependencyAssumptionIgnored means the path makes no assumption about this + // dependency. Its outcome neither gates the merge nor refutes the path. + DependencyAssumptionIgnored DependencyAssumption = "ignored" ) -// DependencyBet is a path's bet on one dependency of its head. -type DependencyBet struct { - // Batch is the dependency batch ID this bet is about. +// PathDependency is one dependency of a path's head, with what the path assumes +// about it. +type PathDependency struct { + // Batch is the dependency batch ID. Batch string - // Bet is how the path treats the dependency (included, excluded, or dropped). - Bet DependencyBetType + // Assumption is what the path assumes about that dependency's outcome. + Assumption DependencyAssumption } -// SpeculationPath is one guess at how a batch's dependencies resolve: a head -// batch plus a bet on each of its dependencies. Every dependency of the head -// appears exactly once, in queue order, so the path is self-describing — its -// full meaning can be read without consulting any external relaxed set or -// dependency list. +// SpeculationPath is one set of assumptions about how a batch's dependencies +// resolve: a head batch plus an assumption about each of its dependencies. +// Every dependency of the head appears exactly once, in queue order, so the +// path is self-describing — its full meaning can be read without consulting any +// external relaxed set or dependency list. type SpeculationPath struct { // Head is the ID of the batch being built along this path. Head string - // Bets is one bet per dependency of Head, in queue order. - Bets []DependencyBet + // Dependencies is one entry per dependency of Head, in queue order. + Dependencies []PathDependency } // ID returns the path's stable identity: a hex-encoded SHA-256 over the head -// and its bets in order. Two paths with the same head and the same ordered -// bets share an ID; any difference in head, dependency, or bet yields a -// different ID. The hash is computed on every call and never cached, so a -// caller that compares IDs repeatedly should keep the result. +// and its dependencies in order. Two paths with the same head and the same +// ordered assumptions share an ID; any difference in head, dependency, or +// assumption yields a different ID. The hash is computed on every call and +// never cached, so a caller that compares IDs repeatedly should keep the +// result. func (p SpeculationPath) ID() string { var b strings.Builder b.WriteString(p.Head) - for _, bet := range p.Bets { + for _, dep := range p.Dependencies { b.WriteByte('\n') - b.WriteString(bet.Batch) + b.WriteString(dep.Batch) b.WriteByte('=') - b.WriteString(string(bet.Bet)) + b.WriteString(string(dep.Assumption)) } sum := sha256.Sum256([]byte(b.String())) return hex.EncodeToString(sum[:]) @@ -124,11 +127,11 @@ func (s SpeculationPathStatus) IsTerminal() bool { // is meaningful only within a single speculation run). type SpeculationPathEntry struct { // ID is the primary key: the hash of the path's content (head plus its - // bets). It always equals Path.ID() — it is materialized here, rather than - // recomputed from Path, because a stored record carries its own key: lookups - // and comparisons read it without rehashing the path. + // assumptions). It always equals Path.ID() — it is materialized here, rather + // than recomputed from Path, because a stored record carries its own key: + // lookups and comparisons read it without rehashing the path. ID string - // Path is the head plus one bet per dependency, in queue order. + // Path is the head plus one assumption per dependency, in queue order. Path SpeculationPath // Status is the lifecycle status of the current build attempt. Status SpeculationPathStatus @@ -149,8 +152,8 @@ type SpeculationPathEntry struct { // SpeculationPathSet is one head's chosen speculation paths under a single // version. It holds both live paths and recently finished ones — finished // entries linger briefly so that a re-run cannot collide with an old build. -// Every path in the set shares the same head and bets over the same ordered -// dependency list. +// Every path in the set shares the same head and assumptions over the same +// ordered dependency list. type SpeculationPathSet struct { // Head is the primary key: the ID of the head batch these paths speculate // on. Every path in the set carries this same head. @@ -179,7 +182,8 @@ const ( // Speculation is one proposed action on one path. A path left as-is has no // Speculation. type Speculation struct { - // Path is the path the action applies to; its ID hashes the head and its bets. + // Path is the path the action applies to; its ID hashes the head and its + // assumptions. Path SpeculationPath // Action is the proposed action (build or cancel). Action PathAction @@ -189,8 +193,11 @@ type Speculation struct { // within a single speculation run. The ranking score orders candidates for that // run only and is never stored — rankings go stale across runs. type CandidatePath struct { - // Path is the candidate: a head plus one bet per dependency. + // Path is the candidate: a head plus one assumption per dependency. Path SpeculationPath - // RankingScore is the ordering score assigned this run; higher sorts first. + // RankingScore is the score the Generator ranked this candidate by. Higher + // sorts first. The scale is the Generator's own — consumers order by it but + // do not interpret it — and it is meaningful only within the run that + // produced it, which is why it is never stored. RankingScore float64 } diff --git a/submitqueue/entity/speculation_test.go b/submitqueue/entity/speculation_test.go index e9d93353..b5946e01 100644 --- a/submitqueue/entity/speculation_test.go +++ b/submitqueue/entity/speculation_test.go @@ -23,18 +23,18 @@ import ( func TestSpeculationPath_ID_Deterministic(t *testing.T) { path := SpeculationPath{ Head: "queueA/batch/3", - Bets: []DependencyBet{ - {Batch: "queueA/batch/1", Bet: BetIncluded}, - {Batch: "queueA/batch/2", Bet: BetExcluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/2", Assumption: DependencyAssumptionFails}, }, } // Same content produces the same ID across separate value instances. same := SpeculationPath{ Head: "queueA/batch/3", - Bets: []DependencyBet{ - {Batch: "queueA/batch/1", Bet: BetIncluded}, - {Batch: "queueA/batch/2", Bet: BetExcluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/2", Assumption: DependencyAssumptionFails}, }, } @@ -45,9 +45,9 @@ func TestSpeculationPath_ID_Deterministic(t *testing.T) { func TestSpeculationPath_ID_Sensitivity(t *testing.T) { base := SpeculationPath{ Head: "queueA/batch/3", - Bets: []DependencyBet{ - {Batch: "queueA/batch/1", Bet: BetIncluded}, - {Batch: "queueA/batch/2", Bet: BetIncluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/2", Assumption: DependencyAssumptionSucceeds}, }, } @@ -58,17 +58,17 @@ func TestSpeculationPath_ID_Sensitivity(t *testing.T) { { name: "different head", path: SpeculationPath{ - Head: "queueA/batch/4", - Bets: base.Bets, + Head: "queueA/batch/4", + Dependencies: base.Dependencies, }, }, { - name: "different bet type", + name: "different assumption", path: SpeculationPath{ Head: base.Head, - Bets: []DependencyBet{ - {Batch: "queueA/batch/1", Bet: BetIncluded}, - {Batch: "queueA/batch/2", Bet: BetExcluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/2", Assumption: DependencyAssumptionFails}, }, }, }, @@ -76,24 +76,24 @@ func TestSpeculationPath_ID_Sensitivity(t *testing.T) { name: "different dependency", path: SpeculationPath{ Head: base.Head, - Bets: []DependencyBet{ - {Batch: "queueA/batch/1", Bet: BetIncluded}, - {Batch: "queueA/batch/9", Bet: BetIncluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/9", Assumption: DependencyAssumptionSucceeds}, }, }, }, { - name: "different bet order", + name: "different dependency order", path: SpeculationPath{ Head: base.Head, - Bets: []DependencyBet{ - {Batch: "queueA/batch/2", Bet: BetIncluded}, - {Batch: "queueA/batch/1", Bet: BetIncluded}, + Dependencies: []PathDependency{ + {Batch: "queueA/batch/2", Assumption: DependencyAssumptionSucceeds}, + {Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}, }, }, }, { - name: "no bets", + name: "no dependencies", path: SpeculationPath{ Head: base.Head, }, @@ -131,18 +131,18 @@ func TestSpeculationPathStatus_IsTerminal(t *testing.T) { func TestSpeculationPathEntry_IDDerivesFromPath(t *testing.T) { path := SpeculationPath{ - Head: "queueA/batch/2", - Bets: []DependencyBet{{Batch: "queueA/batch/1", Bet: BetIncluded}}, + Head: "queueA/batch/2", + Dependencies: []PathDependency{{Batch: "queueA/batch/1", Assumption: DependencyAssumptionSucceeds}}, } // A well-formed entry keys itself by its path's content hash. entry := SpeculationPathEntry{ID: path.ID(), Path: path} assert.Equal(t, entry.Path.ID(), entry.ID) - // A different path (same head, different bet) is a different entry. + // A different path (same head, different assumption) is a different entry. other := SpeculationPath{ - Head: path.Head, - Bets: []DependencyBet{{Batch: "queueA/batch/1", Bet: BetExcluded}}, + Head: path.Head, + Dependencies: []PathDependency{{Batch: "queueA/batch/1", Assumption: DependencyAssumptionFails}}, } assert.NotEqual(t, entry.ID, other.ID()) } diff --git a/submitqueue/extension/speculation/speculator/README.md b/submitqueue/extension/speculation/speculator/README.md index 835af489..f96b34ed 100644 --- a/submitqueue/extension/speculation/speculator/README.md +++ b/submitqueue/extension/speculation/speculator/README.md @@ -6,7 +6,7 @@ The `speculator` package defines the one speculation extension the speculate con `Cancel` is a `Speculator`'s only cancel power — preempting an in-flight path to free budget for a better candidate. Correctness cancels (refuting a path whose bet a resolved dependency broke, and batch cancellation) belong to the controller and are not routed through the extension. -Like the other extensions, a `Speculator` is selected **per queue** by the wiring layer through the `Config` (queue name) and `Factory` interface. Budget, depth bound, clock, and any extra data are injected at construction by the integrator, not carried on the contract. +Like the other extensions, a `Speculator` is selected **per queue** by the wiring layer through the `Config` (queue name) and `Factory` interface. Budget, clock, and any extra data are injected at construction by the integrator, not carried on the contract. ## Adding a backend diff --git a/submitqueue/extension/speculation/speculator/speculator.go b/submitqueue/extension/speculation/speculator/speculator.go index 4501ef2c..acf7237f 100644 --- a/submitqueue/extension/speculation/speculator/speculator.go +++ b/submitqueue/extension/speculation/speculator/speculator.go @@ -61,8 +61,8 @@ type Config struct { } // Factory builds the Speculator for a queue. Implementations are provided by -// integrators (and tests) and inject whatever they need — budget, depth bound, -// clock, and any extra data — at construction. +// integrators (and tests) and inject whatever they need — budget, clock, and +// any extra data — at construction. type Factory interface { // For returns the Speculator for the given queue. For(cfg Config) (Speculator, error)