Skip to content

Commit c22cd35

Browse files
committed
feat(speculation): add enumerator + dependency-limit extensions
Add the first speculation seam and its limit counterpart from the speculation RFC, as vendor-agnostic extension interfaces under submitqueue/extension/speculation/. enumerator: given a batch and its ordered active dependencies, mechanically lists the candidate Base/Head paths — pure, deterministic, and purely structural. It sets no score and no status; the controller stamps status on persist and the scorer fills score. (Its earlier scoring responsibility is dropped here, per the RFC, and moves to the scorer seam.) dependencylimit: the "how much" policy bounding how many active (in-flight) dependencies a batch may speculate over. It is the eligibility gate before enumeration; unlike the other speculation limits it is controller-held rather than injected into a seam, keeping the enumerator pure. The value is signal-driven, not a fixed constant. Each follows the repo extension contract (conflict.Analyzer reference shape): Factory.For(Config) (T, error) with Config carrying only QueueName; behavioral knobs and limit signals are integrator-injected at construction. Includes READMEs, gomock packages, and Makefile mock-gen wiring. Interfaces only; concrete impls and controller wiring are deferred.
1 parent aae4388 commit c22cd35

17 files changed

Lines changed: 642 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/validator/... ./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/validator/... ./platform/consumer/... ./submitqueue/core/changeset/... ./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 = ["dependencylimit.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit",
7+
visibility = ["//visibility:public"],
8+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Speculation Dependency Limit
2+
3+
Vendor-agnostic "how much" policy that bounds how many **active** (in-flight, non-terminal) dependencies a batch may speculate over.
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+
## Dependency Limit
8+
9+
Speculation splits into *decision seams* (what to build) and *limit policies* (how much to allow). The dependency limit is the first limit: it is the **eligibility gate** for speculation. A batch becomes eligible to enumerate only when its count of active dependencies is at or below the current limit; otherwise it waits. Nothing is dropped — as dependencies land they leave the active set, the count shrinks, and the batch is admitted. The gate applies even to the fully-stacked happy path, so a very long chain is not speculated in full at once.
10+
11+
The value is **signal-driven**, not a fixed constant. Its primary input is the build system's available capacity, so a period of CI pressure can shrink how deep the queue speculates, but a policy may also weigh historical pass rates, cost budgets, time of day, or an experiment toggle. Because the value is dynamic, a change to the limit alone — not only a landing dependency or a DAG change — can newly admit a waiting batch.
12+
13+
Unlike the selection and prioritization limits, the dependency limit is **not injected into a decision seam**. It gates eligibility *before* enumeration and needs active-dependency reconciliation, which is controller orchestration — so the controller holds it, consults it on every respeculate, and applies it, keeping the enumerator pure.
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, historical metrics, config — are injected at construction by the integrator in the wiring layer. Computing the limit itself takes no further inputs.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 dependencylimit
16+
17+
//go:generate mockgen -source=dependencylimit.go -destination=mock/dependencylimit_mock.go -package=mock
18+
19+
import "context"
20+
21+
// DependencyLimit is the "how much" policy that bounds how many active
22+
// (in-flight, non-terminal) dependencies a batch may speculate over.
23+
//
24+
// It is the eligibility gate for speculation: a batch becomes eligible to
25+
// enumerate only when its count of active dependencies is at or below the
26+
// current limit; otherwise it waits, and is admitted later as predecessors land
27+
// and leave the active set. The limit is a bound, not a trim — nothing is
28+
// dropped from a batch's base.
29+
//
30+
// The value is dynamic: it may change between calls — not only when a
31+
// dependency lands — so a change alone can newly admit a waiting batch, and the
32+
// controller re-consults it on every respeculate rather than caching it.
33+
//
34+
// This limit is the exception among the speculation limits: it gates eligibility
35+
// *before* enumeration and needs active-dependency reconciliation, which is
36+
// controller orchestration — so the controller holds and applies it, rather than
37+
// it being injected into a decision seam. The enumerator stays pure.
38+
type DependencyLimit interface {
39+
// Limit returns the current maximum number of active dependencies a batch
40+
// may speculate over. The controller compares a batch's active-dependency
41+
// count against this to decide eligibility. It takes no parameters; anything
42+
// an implementation needs is injected at construction.
43+
Limit(ctx context.Context) (int, error)
44+
}
45+
46+
// Config carries the per-queue identity handed to a Factory. The system knows
47+
// only the queue name; everything a policy needs to compute the limit (a
48+
// capacity feed, historical metrics, config) is injected at construction by the
49+
// integrator.
50+
type Config struct {
51+
// QueueName identifies the queue this DependencyLimit serves.
52+
QueueName string
53+
}
54+
55+
// Factory builds the DependencyLimit for a queue. Implementations are provided
56+
// by integrators (and tests) and inject whatever signals they need at
57+
// construction.
58+
type Factory interface {
59+
// For returns the DependencyLimit for the given queue.
60+
For(cfg Config) (DependencyLimit, error)
61+
}
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/dependencylimit/fake",
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 = ["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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 dependencylimit.DependencyLimit for tests
16+
// and examples. New sets the value returned by Limit; FailWith injects an error
17+
// on every call. It is intended for examples and tests only, never production.
18+
package fake
19+
20+
import (
21+
"context"
22+
23+
"github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit"
24+
)
25+
26+
// DependencyLimit is a programmable dependencylimit.DependencyLimit.
27+
type DependencyLimit struct {
28+
limit int
29+
err error
30+
}
31+
32+
// New returns a fake DependencyLimit whose Limit returns the given value.
33+
func New(limit int) *DependencyLimit {
34+
return &DependencyLimit{limit: limit}
35+
}
36+
37+
// FailWith makes every Limit call return err.
38+
func (l *DependencyLimit) FailWith(err error) *DependencyLimit {
39+
l.err = err
40+
return l
41+
}
42+
43+
// Limit returns the configured value, or the injected error if FailWith was set.
44+
func (l *DependencyLimit) Limit(_ context.Context) (int, error) {
45+
if l.err != nil {
46+
return 0, l.err
47+
}
48+
return l.limit, nil
49+
}
50+
51+
// ensure the fake satisfies the interface.
52+
var _ dependencylimit.DependencyLimit = (*DependencyLimit)(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(3).Limit(context.Background())
28+
require.NoError(t, err)
29+
assert.Equal(t, 3, got)
30+
}
31+
32+
func TestLimit_FailWith(t *testing.T) {
33+
sentinel := errors.New("boom")
34+
_, err := New(3).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 = ["dependencylimit_mock.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/dependencylimit/mock",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//submitqueue/extension/speculation/dependencylimit:go_default_library",
10+
"@org_uber_go_mock//gomock:go_default_library",
11+
],
12+
)

submitqueue/extension/speculation/dependencylimit/mock/dependencylimit_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: 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 = ["enumerator.go"],
6+
importpath = "github.com/uber/submitqueue/submitqueue/extension/speculation/enumerator",
7+
visibility = ["//visibility:public"],
8+
deps = ["//submitqueue/entity:go_default_library"],
9+
)

0 commit comments

Comments
 (0)