Skip to content

Commit bce0f4e

Browse files
authored
feat(speculation): add prioritizer + prioritization-limit extensions (#320)
Add the queue-wide prioritizer seam and its limit counterpart from the speculation RFC, as vendor-agnostic extension interfaces under submitqueue/extension/speculation/. prioritizer: selection is per batch and blind to other batches, so it cannot ration a shared build budget. The prioritizer sees every path across the queue's in-flight batches that runs or wants to run (Selected / Prioritized / Building, each carrying its ID and score), ranks them — scores are probabilities in [0, 1] per the path scorer's contract — and returns sparse decisions naming paths by ID: Promote to admit under the budget, Cancel to preempt, at most one decision per path. Whether it preempts at all is implementation policy (sticky-slots vs preemptive). It never writes: the controller maps each decision back to its tree and applies the status transition under that tree's optimistic lock. Prioritized thus means exactly "admitted under the queue's build budget, cleared to build, not yet building". prioritizationlimit: the "how much" policy bounding the queue's concurrent speculation builds — the budget the prioritizer admits against. Injected into the prioritizer at construction and applied by it, keeping the interface limit-free; the value is signal-driven, not a fixed constant. Each follows the repo extension contract: Factory.For(Config) (T, error) with Config carrying only QueueName. Includes READMEs, gomock packages, and programmable fakes. Interfaces only; concrete impls and controller wiring are deferred. ## Stack 1. #337 1. #315 1. #316 1. #317 1. @ #320 1. #331 1. #332 1. #333
1 parent be12a19 commit bce0f4e

17 files changed

Lines changed: 647 additions & 1 deletion

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ local-stovepipe-stop: ## Stop the Stovepipe service
364364

365365
mocks: ## Generate mock files using mockgen
366366
@echo "Generating mocks..."
367-
@$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/mergechecker/... ./submitqueue/extension/pusher/... ./submitqueue/extension/scorer/... ./submitqueue/extension/conflict/... ./submitqueue/extension/speculation/enumerator/... ./submitqueue/extension/speculation/dependencylimit/... ./submitqueue/extension/speculation/selector/... ./submitqueue/extension/speculation/selectionlimit/... ./submitqueue/extension/validator/... ./submitqueue/extension/speculation/pathscorer/... ./platform/consumer/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/...
367+
@$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/mergechecker/... ./submitqueue/extension/pusher/... ./submitqueue/extension/scorer/... ./submitqueue/extension/conflict/... ./submitqueue/extension/speculation/enumerator/... ./submitqueue/extension/speculation/dependencylimit/... ./submitqueue/extension/speculation/selector/... ./submitqueue/extension/speculation/selectionlimit/... ./submitqueue/extension/speculation/prioritizer/... ./submitqueue/extension/speculation/prioritizationlimit/... ./submitqueue/extension/validator/... ./submitqueue/extension/speculation/pathscorer/... ./platform/consumer/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/...
368368
@echo "Mocks generated successfully!"
369369

370370
proto: ## Generate protobuf files from .proto definitions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["prioritizationlimit.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizationlimit",
7+
visibility = ["//visibility:public"],
8+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Prioritization Limit
2+
3+
Vendor-agnostic "how much" policy that bounds how many builds a queue may run at once — the queue's concurrent-build budget.
4+
5+
See the [Speculation RFC](/doc/rfc/submitqueue/speculation.md) for the end-to-end design and how limits fit into the two-layer speculation model.
6+
7+
## Prioritization Limit
8+
9+
The prioritization limit is the [prioritizer](../prioritizer)'s companion. The prioritizer decides **which** of the queue's pending builds run — its ranking across all in-flight batches; the prioritization limit decides **how many** fit at once. It is the queue-wide resource knob, the ultimate cap on speculation's demand on CI.
10+
11+
The value is **signal-driven**, not a fixed constant. Its primary input is the build system's available capacity, but a policy may also weigh cost budgets, time of day, or an experiment toggle.
12+
13+
It is **injected into the prioritizer** at construction and called by it, never passed as a method parameter — following the repo's extension-contract pattern, keeping the prioritizer interface limit-free and stable, and letting the limit be swapped independently of prioritizer logic.
14+
15+
## Factory
16+
17+
A per-queue factory returns the limit policy for a queue, following the repo's extension contract. It is handed only the queue identity; the signals a policy weighs — a capacity feed, cost budgets, config — are injected at construction by the integrator in the wiring layer, which is also where the limit is handed to the prioritizer. Computing the limit itself takes no further inputs.
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 = ["fake.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizationlimit/fake",
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 = ["fake_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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 fake provides a programmable prioritizationlimit.PrioritizationLimit
16+
// for tests and examples. New sets the value returned by Limit; FailWith injects
17+
// an error on every call. It is intended for examples and tests only, never
18+
// production.
19+
package fake
20+
21+
import (
22+
"context"
23+
24+
"github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizationlimit"
25+
)
26+
27+
// PrioritizationLimit is a programmable prioritizationlimit.PrioritizationLimit.
28+
type PrioritizationLimit struct {
29+
limit int
30+
err error
31+
}
32+
33+
// New returns a fake PrioritizationLimit whose Limit returns the given value.
34+
func New(value int) *PrioritizationLimit {
35+
return &PrioritizationLimit{limit: value}
36+
}
37+
38+
// FailWith makes every Limit call return err.
39+
func (l *PrioritizationLimit) FailWith(err error) *PrioritizationLimit {
40+
l.err = err
41+
return l
42+
}
43+
44+
// Limit returns the configured value, or the injected error if FailWith was set.
45+
func (l *PrioritizationLimit) Limit(_ context.Context) (int, error) {
46+
if l.err != nil {
47+
return 0, l.err
48+
}
49+
return l.limit, nil
50+
}
51+
52+
// ensure the fake satisfies the interface.
53+
var _ prioritizationlimit.PrioritizationLimit = (*PrioritizationLimit)(nil)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 fake
16+
17+
import (
18+
"context"
19+
"errors"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestLimit_ReturnsConfiguredValue(t *testing.T) {
27+
got, err := New(8).Limit(context.Background())
28+
require.NoError(t, err)
29+
assert.Equal(t, 8, got)
30+
}
31+
32+
func TestLimit_FailWith(t *testing.T) {
33+
sentinel := errors.New("boom")
34+
_, err := New(8).FailWith(sentinel).Limit(context.Background())
35+
require.ErrorIs(t, err, sentinel)
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["prioritizationlimit_mock.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizationlimit/mock",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//submitqueue/extension/speculation/prioritizationlimit:go_default_library",
10+
"@org_uber_go_mock//gomock:go_default_library",
11+
],
12+
)

submitqueue/extension/speculation/prioritizationlimit/mock/prioritizationlimit_mock.go

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 prioritizationlimit
16+
17+
//go:generate mockgen -source=prioritizationlimit.go -destination=mock/prioritizationlimit_mock.go -package=mock
18+
19+
import "context"
20+
21+
// PrioritizationLimit is the "how much" policy that bounds how many builds a
22+
// queue may run at once — the queue's concurrent-build budget.
23+
//
24+
// It is the prioritizer's companion: the prioritizer decides *which* of the
25+
// queue's pending builds run (its ranking across all in-flight batches); the
26+
// prioritization limit decides *how many* fit at once. It is the queue-wide
27+
// resource knob, the ultimate cap on speculation's demand on CI.
28+
//
29+
// The value is dynamic: it may change between calls, so the prioritizer reads it
30+
// each round rather than caching it.
31+
//
32+
// It is injected into the prioritizer at construction and called by it, never
33+
// passed as a method parameter, keeping the prioritizer interface limit-free and
34+
// stable.
35+
type PrioritizationLimit interface {
36+
// Limit returns the current maximum number of concurrent builds for the
37+
// queue. The prioritizer admits at most this many candidates. It takes no
38+
// parameters; anything an implementation needs is injected at construction.
39+
Limit(ctx context.Context) (int, error)
40+
}
41+
42+
// Config carries the per-queue identity handed to a Factory. The system knows
43+
// only the queue name; everything a policy needs to compute the limit (a
44+
// capacity feed, cost budgets, config) is injected at construction by the
45+
// integrator.
46+
type Config struct {
47+
// QueueName identifies the queue this PrioritizationLimit serves.
48+
QueueName string
49+
}
50+
51+
// Factory builds the PrioritizationLimit for a queue. Implementations are
52+
// provided by integrators (and tests) and inject whatever signals they need at
53+
// construction.
54+
type Factory interface {
55+
// For returns the PrioritizationLimit for the given queue.
56+
For(cfg Config) (PrioritizationLimit, error)
57+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["prioritizer.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/prioritizer",
7+
visibility = ["//visibility:public"],
8+
deps = ["//submitqueue/entity:go_default_library"],
9+
)

0 commit comments

Comments
 (0)