Skip to content

Commit e32cca4

Browse files
committed
feat(speculation): add parity default impls for the speculation seams
## Summary ### Why? The speculation seam contracts (enumerator, selector, prioritizer, limits) have no real implementations yet — only mocks and programmable fakes. Integrating the seams into the orchestrator needs defaults that reproduce the existing single-chain behavior exactly (one speculative path per batch: the full dependency chain, promoted and admitted unconditionally), so the pipeline can switch to tree-driven speculation with zero behavior change and the e2e suite staying green. The path scorer — the one seam with a real model rather than a parity stub — is split into its own follow-up change for focused review. ### What? Five impl packages under `submitqueue/extension/speculation/`, each a plain `New(...)` constructor returning the contract interface (no factories, no queue routing — that stays in wiring): - `enumerator/chain` — one path per batch: `{Base: deps in order, Head: batch}`; structure only, controller stamps status. - `selector/all` — promotes every candidate path. - `prioritizer/sticky` — sticky slots: running/prioritized paths hold their slot, top-scored selected paths fill free budget, never preempts; deterministic tie-break for stable rounds. - `dependencylimit/static`, `prioritizationlimit/static` — fixed limits injected at construction. ## Test Plan ✅ `make gazelle && make fmt && bazel test //submitqueue/extension/speculation/...`. Unit coverage: chain ordering (incl. empty deps), candidate-only selection, sticky budget accounting (running paths consume slots, zero-free floor, tie-break determinism across permutations), static limit passthrough.
1 parent 2551cf4 commit e32cca4

20 files changed

Lines changed: 750 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["static.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit/static",
7+
visibility = ["//visibility:public"],
8+
deps = ["//submitqueue/extension/speculation/dependencylimit:go_default_library"],
9+
)
10+
11+
go_test(
12+
name = "go_default_test",
13+
srcs = ["static_test.go"],
14+
embed = [":go_default_library"],
15+
deps = [
16+
"@com_github_stretchr_testify//assert:go_default_library",
17+
"@com_github_stretchr_testify//require:go_default_library",
18+
],
19+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Static Dependency Limit
2+
3+
The static `dependencylimit.DependencyLimit` is constructed with a single integer and returns it, unchanged, from every call to `Limit`. It never errors and never consults any external signal — the simplest possible dependency limit, useful for wiring tests, local development, and any queue whose eligibility bound is a fixed operational constant rather than one derived from live capacity or other signals.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package static provides a dependencylimit.DependencyLimit that always
16+
// returns a fixed, construction-time value.
17+
package static
18+
19+
import (
20+
"context"
21+
22+
"github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit"
23+
)
24+
25+
// staticLimit is a dependencylimit.DependencyLimit that always returns a
26+
// fixed value.
27+
type staticLimit struct {
28+
// limit is the fixed maximum active-dependency count returned by every
29+
// call to Limit.
30+
limit int
31+
}
32+
33+
// New returns a dependencylimit.DependencyLimit whose Limit always returns
34+
// limit.
35+
func New(limit int) dependencylimit.DependencyLimit {
36+
return staticLimit{limit: limit}
37+
}
38+
39+
// Limit returns the fixed value given to New. It never errors.
40+
func (l staticLimit) Limit(_ context.Context) (int, error) {
41+
return l.limit, nil
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package static
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestLimit_ReturnsConfiguredValue(t *testing.T) {
26+
got, err := New(4).Limit(context.Background())
27+
require.NoError(t, err)
28+
assert.Equal(t, 4, got)
29+
}
30+
31+
func TestLimit_ZeroValue(t *testing.T) {
32+
got, err := New(0).Limit(context.Background())
33+
require.NoError(t, err)
34+
assert.Equal(t, 0, got)
35+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["chain.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator/chain",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//submitqueue/entity:go_default_library",
10+
"//submitqueue/extension/speculation/enumerator:go_default_library",
11+
],
12+
)
13+
14+
go_test(
15+
name = "go_default_test",
16+
srcs = ["chain_test.go"],
17+
embed = [":go_default_library"],
18+
deps = [
19+
"//submitqueue/entity:go_default_library",
20+
"@com_github_stretchr_testify//assert:go_default_library",
21+
"@com_github_stretchr_testify//require:go_default_library",
22+
],
23+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Chain Enumerator
2+
3+
Given a batch and its active dependency batches in arrival order, the chain `enumerator.Enumerator` produces exactly one candidate path: the batch built directly on top of the full chain of those dependencies, in the order given. It never branches, so a batch's tree only ever contains this single path. An empty dependency list still yields one path, whose base is empty — the batch built directly on the target branch.
4+
5+
This is deliberately the least interesting enumerator: it does not weigh which dependencies to include or exclude, does not consider dropping a shaky predecessor, and does not produce a "build alone" alternative alongside the chained one. It is the working, deterministic single-chain baseline — a richer enumerator (for example, one that also offers a build-alone fallback path per dependency) can replace or supplement it without changing the contract.
6+
7+
As with any `enumerator.Enumerator`, the returned path carries structure only: `Score` and `Status` are left at their zero values for the controller to fill in.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package chain provides an enumerator.Enumerator that enumerates a batch
16+
// as exactly one path, built directly on top of the full ordered chain of
17+
// its active dependencies. It is the single-chain baseline; a multi-path
18+
// enumerator (e.g. one adding build-alone fallback paths) can replace or
19+
// supplement it without changing the contract.
20+
package chain
21+
22+
import (
23+
"context"
24+
25+
"github.com/uber/submitqueue/submitqueue/entity"
26+
"github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator"
27+
)
28+
29+
// chainEnumerator is an enumerator.Enumerator that always enumerates a single
30+
// path per batch.
31+
type chainEnumerator struct{}
32+
33+
// New returns an enumerator.Enumerator that enumerates exactly one path per
34+
// batch: the path's Base is deps in the given order and its Head is the
35+
// batch itself.
36+
func New() enumerator.Enumerator {
37+
return chainEnumerator{}
38+
}
39+
40+
// Enumerate returns a tree for batchID with exactly one path whose Base
41+
// preserves deps' order and whose Head is batchID — including when deps is
42+
// empty, in which case the single path has an empty Base (the batch builds
43+
// directly on the target branch). Score and Status are left at their zero
44+
// values; the controller stamps Status on persist and calls the scorer to
45+
// fill Score. Version is left zero; the controller owns it.
46+
func (chainEnumerator) Enumerate(_ context.Context, batchID string, deps []entity.Batch) (entity.SpeculationTree, error) {
47+
var base []string
48+
for _, dep := range deps {
49+
base = append(base, dep.ID)
50+
}
51+
return entity.SpeculationTree{
52+
BatchID: batchID,
53+
Paths: []entity.SpeculationPathInfo{
54+
{
55+
Path: entity.SpeculationPath{
56+
Base: base,
57+
Head: batchID,
58+
},
59+
},
60+
},
61+
}, nil
62+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package chain
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
"github.com/uber/submitqueue/submitqueue/entity"
24+
)
25+
26+
func TestEnumerate(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
batchID string
30+
deps []entity.Batch
31+
want entity.SpeculationTree
32+
}{
33+
{
34+
name: "no deps yields one path with an empty base",
35+
batchID: "q/batch/1",
36+
deps: nil,
37+
want: entity.SpeculationTree{
38+
BatchID: "q/batch/1",
39+
Paths: []entity.SpeculationPathInfo{
40+
{Path: entity.SpeculationPath{Head: "q/batch/1"}},
41+
},
42+
},
43+
},
44+
{
45+
name: "deps preserve input order in base",
46+
batchID: "q/batch/4",
47+
deps: []entity.Batch{
48+
{ID: "q/batch/3"},
49+
{ID: "q/batch/1"},
50+
{ID: "q/batch/2"},
51+
},
52+
want: entity.SpeculationTree{
53+
BatchID: "q/batch/4",
54+
Paths: []entity.SpeculationPathInfo{
55+
{Path: entity.SpeculationPath{
56+
Base: []string{"q/batch/3", "q/batch/1", "q/batch/2"},
57+
Head: "q/batch/4",
58+
}},
59+
},
60+
},
61+
},
62+
}
63+
64+
e := New()
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
got, err := e.Enumerate(context.Background(), tt.batchID, tt.deps)
68+
require.NoError(t, err)
69+
assert.Equal(t, tt.want, got)
70+
require.Len(t, got.Paths, 1)
71+
assert.Equal(t, tt.batchID, got.BatchID)
72+
assert.Equal(t, tt.batchID, got.Paths[0].Path.Head)
73+
assert.Zero(t, got.Paths[0].Score)
74+
assert.Empty(t, got.Paths[0].Status)
75+
assert.Zero(t, got.Version)
76+
})
77+
}
78+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["static.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizationlimit/static",
7+
visibility = ["//visibility:public"],
8+
deps = ["//submitqueue/extension/speculation/prioritizationlimit:go_default_library"],
9+
)
10+
11+
go_test(
12+
name = "go_default_test",
13+
srcs = ["static_test.go"],
14+
embed = [":go_default_library"],
15+
deps = [
16+
"@com_github_stretchr_testify//assert:go_default_library",
17+
"@com_github_stretchr_testify//require:go_default_library",
18+
],
19+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Static Prioritization Limit
2+
3+
The static `prioritizationlimit.PrioritizationLimit` is constructed with a single integer and returns it, unchanged, from every call to `Limit`. It never errors and never consults any external signal — the simplest possible prioritization limit, useful for wiring tests, local development, and any queue whose concurrent-build budget is a fixed operational constant rather than one derived from live CI capacity or cost signals.

0 commit comments

Comments
 (0)