Skip to content

Commit bdb5d8a

Browse files
committed
feat(demo): Add an ANALYZER=tango option to the local demo
1 parent fd47b5b commit bdb5d8a

15 files changed

Lines changed: 289 additions & 22 deletions

File tree

‎Makefile‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ export SQ_PROVIDER_CONFIG_DIR ?= $(REPO_ROOT)/service/submitqueue/demo/provider/
6464
PROVIDER_COMPOSE_FILE_fake = $(abspath service/submitqueue/docker-compose.fake.yml)
6565
PROVIDER_COMPOSE_FILE_git = $(abspath service/submitqueue/docker-compose.git.yml)
6666
PROVIDER_COMPOSE_FILE_github = $(abspath service/submitqueue/docker-compose.provider.yml)
67-
PROVIDER_COMPOSE_FILE = $(PROVIDER_COMPOSE_FILE_$(PROVIDER))
67+
# What answers conflict analysis, chosen independently of the provider. Empty
68+
# keeps the provider's own analyzer; tango adds a fake Tango server and needs
69+
# PROVIDER=git, whose repository it reads targets from.
70+
ANALYZER ?=
71+
ANALYZER_COMPOSE_FILE_tango = $(abspath service/submitqueue/docker-compose.tango.yml)
72+
ANALYZER_COMPOSE_FILE = $(ANALYZER_COMPOSE_FILE_$(ANALYZER))
73+
PROVIDER_COMPOSE_FILE = $(PROVIDER_COMPOSE_FILE_$(PROVIDER))$(if $(ANALYZER_COMPOSE_FILE), -f $(ANALYZER_COMPOSE_FILE))
6874

6975
# Where PROVIDER=git keeps the bare repository it lands into. Outside the
7076
# repository, so a demo leaves nothing in a checkout, and bind-mounted rather
@@ -165,7 +171,7 @@ build: ## Build all services and examples
165171
# Build Linux binaries required for the local docker-compose flows (local-*).
166172
# Bazel-run integration/e2e tests do NOT need these: they build the service
167173
# binaries hermetically as data dependencies.
168-
build-all-linux: build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-linux build-runway-linux ## Build all Linux binaries for Docker
174+
build-all-linux: build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-linux build-runway-linux build-tangofake-linux ## Build all Linux binaries for Docker
169175
@echo "All Linux binaries ready for Docker"
170176

171177
build-runway-linux: ## Build Runway Linux binary for Docker
@@ -389,7 +395,7 @@ local-submitqueue-clean: ## Stop the stack and remove its volumes, images, and P
389395
@echo "Cleaning all services and data..."
390396
@# The overlay is named so that volumes it declares — Runway's checkouts —
391397
@# are removed too, rather than surviving as an orphan.
392-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local
398+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down --remove-orphans -v --rmi local
393399
@rm -rf "$(SQ_GIT_SANDBOX_DIR)"
394400
@echo "All services, volumes, images, and $(SQ_GIT_SANDBOX_DIR) removed."
395401

@@ -534,14 +540,18 @@ local-submitqueue-restart: build-all-linux ## Restart all services (rebuild and
534540
@echo "Services restarted!"
535541
@make local-submitqueue-ps
536542

537-
local-submitqueue-start: build-all-linux web-image-load ## Start full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN)
543+
local-submitqueue-start: build-all-linux web-image-load ## Start full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN; ANALYZER=tango with PROVIDER=git adds a fake Tango server)
544+
@test -z "$(ANALYZER)" || test -n "$(ANALYZER_COMPOSE_FILE)" \
545+
|| { echo "Unknown ANALYZER '$(ANALYZER)'; the only choice is tango"; exit 2; }
546+
@test -z "$(ANALYZER)" || test "$(PROVIDER)" = "git" \
547+
|| { echo "ANALYZER=$(ANALYZER) needs PROVIDER=git, got '$(PROVIDER)'"; exit 2; }
538548
@echo "Starting full stack against provider '$(PROVIDER)' ($(SQ_PROVIDER_CONFIG_DIR))..."
539549
@test -f "$(SQ_PROVIDER_CONFIG_DIR)/merge.yaml" \
540550
|| { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/merge.yaml not found"; exit 2; }
541551
@test -n "$(PROVIDER_COMPOSE_FILE)" \
542552
|| { echo "Provider '$(PROVIDER)' has no compose overlay; add PROVIDER_COMPOSE_FILE_$(PROVIDER) to the Makefile"; exit 2; }
543553
@if [ "$(PROVIDER)" = "git" ]; then \
544-
$(BAZEL) run //tool/gitsandbox -- -sandbox-dir "$(SQ_GIT_SANDBOX_DIR)" || exit 1; \
554+
$(BAZEL) run //tool/gitsandbox -- -sandbox-dir "$(SQ_GIT_SANDBOX_DIR)" $(if $(ANALYZER),-targets) || exit 1; \
545555
fi
546556
@# Rootless Docker maps container root to the host user; rootful Docker needs
547557
@# the host UID:GID explicitly, or the services write files into the sandbox
@@ -572,7 +582,7 @@ local-submitqueue-start: build-all-linux web-image-load ## Start full stack (PRO
572582

573583
local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sandbox; the databases do not survive)
574584
@echo "Stopping SubmitQueue services..."
575-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
585+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down --remove-orphans
576586
@# Both MySQL services mount anonymous volumes, so `down` detaches them and
577587
@# the next `up` creates fresh ones. Saying "data preserved" here would be
578588
@# read as "your requests are still there", which they are not.
@@ -584,7 +594,7 @@ local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sand
584594

585595
local-stop: ## Stop every local stack — SubmitQueue, Stovepipe, and Runway
586596
@echo "Stopping all services..."
587-
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
597+
@$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down --remove-orphans
588598
@$(COMPOSE) -f $(STOVEPIPE_COMPOSE_FILE) -f $(STOVEPIPE_WEB_COMPOSE_FILE) -p $(STOVEPIPE_LOCAL_PROJECT) down
589599
@$(COMPOSE) -f $(RUNWAY_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) down
590600
@echo "Services stopped. Anonymous database volumes are not reused on the next start."

‎doc/howto/QUICKSTART.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,30 @@ Three changes, one more ref update than before.
259259

260260
The sandbox survives `make local-submitqueue-stop`, so what landed is still there to look at. `make local-submitqueue-clean` removes it.
261261

262+
## Decide conflicts with Tango
263+
264+
`PROVIDER=git` decides that two changes conflict by comparing the directories they touch. An analyzer built on a build graph can see more: a change to a library conflicts with a change to anything built on it, though their paths never overlap. `ANALYZER=tango` swaps the analyzer for one that asks a Tango server which build targets each batch affects, and runs a fake Tango server beside the stack so it needs no build system.
265+
266+
```bash
267+
make local-submitqueue-stop
268+
make local-submitqueue-start PROVIDER=git ANALYZER=tango
269+
make demo-requests
270+
make local-submitqueue-stop PROVIDER=git ANALYZER=tango
271+
```
272+
273+
Starting with `ANALYZER=tango` also commits a `TARGET` file into each `demo/NN` folder of the sandbox, on top of whatever is already there. `demo/00` and `demo/01` are libraries, and every other folder depends on one of them: even folders on `demo/00`, odd folders on `demo/01`. Pass the same `ANALYZER` when stopping, so the fake server is stopped with the rest. Only the folders `demo/00` to `demo/09` have targets, so `make demo-requests` refuses a `FOLDERS` above 10 against a sandbox seeded this way, because changes in a folder without a target would never conflict.
274+
275+
Look at what the analyzer recorded, the same way as in the speculation section above:
276+
277+
```bash
278+
docker exec submitqueue-mysql-app-1 mysql -uroot -proot submitqueue \
279+
-N -e "select batch_id, dependents from batch_dependent where dependents != '[]'"
280+
docker exec submitqueue-mysql-app-1 mysql -uroot -proot submitqueue \
281+
-N -e "select count(*) from target_signature"
282+
```
283+
284+
The second count has a row for every batch analyzed, which shows the orchestrator really asked the fake server. Dependencies between batches can now join different folders, where under path overlap they only join batches sharing one.
285+
262286
## Land against GitHub
263287

264288
The last rung is the only one that needs credentials, and the only one where a change is a pull request that a person can review.

‎service/submitqueue/demo/provider/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Pick one with `make local-submitqueue-start PROVIDER=<name>`, which bind-mounts
1919

2020
The three are a ladder, and the rung is the only thing that changes: the same commands land against all of them. `git/` is worth reading first of the two real ones. It is proof that neither half needs a provider — change metadata comes from reading the repository and the merge is the same Runway code path, both against a bare repository addressed by path, with no credential and no API — and it is what the hermetic git E2E (`make e2e-git-test`) runs against.
2121

22+
The conflict analyzer is a separate choice from the provider. `make local-submitqueue-start PROVIDER=git ANALYZER=tango` keeps everything about `git/` and swaps `demo-queue`'s analyzer for Tango, answered by the fake server in [`service/tangofake`](../../../tangofake). It reads [`git/profiles.tango.yaml`](git/profiles.tango.yaml) instead of `profiles.yaml`, and the sandbox gains a `TARGET` file per `demo/NN` folder. Only `PROVIDER=git` supports it, because the fake reads targets from that provider's repository.
23+
2224
Note that the orchestrator and Runway each keep their **own** copy of a queue's repository and configure their own remote for it: the change provider's copy is in `profiles.yaml`, the merger's checkout in `merge.yaml`, and they are independent even when they name the same remote. That is the same shape a deployment has when both point at a remote host.
2325

2426
## Adding a provider

‎service/submitqueue/demo/provider/git/BUILD.bazel‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ filegroup(
88
name = "config",
99
srcs = [
1010
"merge.yaml",
11+
"profiles.tango.yaml",
1112
"profiles.yaml",
1213
],
1314
visibility = ["//test:__subpackages__"],
1415
)
16+
17+
# Read by the orchestrator's config test, so the profile `ANALYZER=tango`
18+
# mounts is checked against the loader that will read it.
19+
exports_files(
20+
["profiles.tango.yaml"],
21+
visibility = ["//service/submitqueue/orchestrator/server:__pkg__"],
22+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Extension profiles for the "git" example with the Tango conflict analyzer:
2+
# `make local-submitqueue-start PROVIDER=git ANALYZER=tango`.
3+
#
4+
# The same repository, change provider and build runner as profiles.yaml; the
5+
# only difference is how demo-queue decides that two batches conflict. Instead
6+
# of comparing paths, it asks a Tango server which build targets each batch
7+
# affects, so batches conflict when their targets overlap. The server here is the
8+
# fake in service/tangofake, which reads the TARGET files that tool/gitsandbox
9+
# commits into the sandbox.
10+
11+
tango:
12+
address: tangofake-service:8080
13+
# Shared by every queue's analyzer.
14+
signatureCacheBytes: 67108864
15+
16+
defaults:
17+
changeProvider: {type: fake}
18+
buildRunner: {type: fake}
19+
analyzer: {type: all}
20+
# Same example factors as ../fake/profiles.yaml.
21+
scorer:
22+
type: evidence
23+
factors:
24+
pathPassed: 10
25+
pathFailed: 0.3
26+
landing: 12
27+
cancelling: 0.1
28+
base:
29+
type: heuristic
30+
31+
queues:
32+
- name: demo-queue
33+
changeProvider:
34+
type: git
35+
git:
36+
remoteUrl: file:///srv/git/sandbox.git
37+
target: main
38+
repoPath: /var/submitqueue/changerepos/demo
39+
analyzer:
40+
type: tango
41+
tango:
42+
# The fake serves one repository and ignores the remote.
43+
remote: sandbox

‎service/submitqueue/demo/requests/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ go_test(
4747
"//platform/base/change/git:go_default_library",
4848
"//platform/git/exec:go_default_library",
4949
"//platform/git/exectest:go_default_library",
50+
"//service/submitqueue/demo:go_default_library",
5051
"@com_github_stretchr_testify//assert:go_default_library",
5152
"@com_github_stretchr_testify//require:go_default_library",
5253
],

‎service/submitqueue/demo/requests/main.go‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ func newChangeSource(ctx context.Context, cfg config) (changeSource, func(), err
221221
if err != nil {
222222
return nil, nil, err
223223
}
224-
src, err := newGitSource(ctx, git, filepath.Join(cfg.sandboxDir, sandboxRepo+".git"), sandboxRepo)
224+
bare := filepath.Join(cfg.sandboxDir, sandboxRepo+".git")
225+
if err := checkFoldersHaveTargets(ctx, git, bare, cfg.base, cfg.folders); err != nil {
226+
return nil, nil, err
227+
}
228+
src, err := newGitSource(ctx, git, bare, sandboxRepo)
225229
if err != nil {
226230
return nil, nil, err
227231
}
@@ -235,6 +239,22 @@ func newChangeSource(ctx context.Context, cfg config) (changeSource, func(), err
235239
return nil, nil, fmt.Errorf("unknown provider %q", cfg.provider)
236240
}
237241

242+
// targetsMarker is the file tool/gitsandbox -targets commits first; it is on
243+
// the base branch only when the stack runs the Tango analyzer.
244+
const targetsMarker = "demo/00/TARGET"
245+
246+
// checkFoldersHaveTargets rejects a folder count the sandbox's targets do not
247+
// cover, since changes in a folder without a target never conflict.
248+
func checkFoldersHaveTargets(ctx context.Context, git, bare, base string, folders int) error {
249+
if folders <= demo.MaxShardDirs {
250+
return nil
251+
}
252+
if err := gitexec.Run(ctx, git, bare, "cat-file", "-e", base+":"+targetsMarker); err != nil {
253+
return nil
254+
}
255+
return fmt.Errorf("-folders %d is above %d, the folders with targets in this sandbox", folders, demo.MaxShardDirs)
256+
}
257+
238258
// sandboxRepo is the repository the git provider's sandbox holds, matching what
239259
// tool/gitsandbox creates and what demo/provider/git/merge.yaml merges into.
240260
const sandboxRepo = "sandbox"

‎service/submitqueue/demo/requests/source_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
gitchange "github.com/uber/submitqueue/platform/base/change/git"
2929
gitexec "github.com/uber/submitqueue/platform/git/exec"
3030
gitexectest "github.com/uber/submitqueue/platform/git/exectest"
31+
"github.com/uber/submitqueue/service/submitqueue/demo"
3132
)
3233

3334
// quietSpec is a changeSpec whose progress notes go nowhere, for tests that
@@ -245,3 +246,40 @@ func TestNewChangeSource_GitRejectsAMissingSandbox(t *testing.T) {
245246
})
246247
require.Error(t, err, "a missing sandbox must say so rather than fail later mid-run")
247248
}
249+
250+
func TestCheckFoldersHaveTargets(t *testing.T) {
251+
git := gitexectest.Git(t)
252+
ctx := context.Background()
253+
plain := sandbox(t, git)
254+
withTargets := sandbox(t, git)
255+
seed := t.TempDir()
256+
require.NoError(t, gitexec.Run(ctx, git, "", "clone", withTargets, seed))
257+
require.NoError(t, gitexec.Run(ctx, git, seed, "config", "user.name", "Test"))
258+
require.NoError(t, gitexec.Run(ctx, git, seed, "config", "user.email", "test@example.invalid"))
259+
require.NoError(t, os.MkdirAll(filepath.Join(seed, "demo", "00"), 0o755))
260+
require.NoError(t, os.WriteFile(filepath.Join(seed, targetsMarker), nil, 0o644))
261+
require.NoError(t, gitexec.Run(ctx, git, seed, "add", "."))
262+
require.NoError(t, gitexec.Run(ctx, git, seed, "commit", "-m", "targets"))
263+
require.NoError(t, gitexec.Run(ctx, git, seed, "push", "origin", "main"))
264+
265+
tests := []struct {
266+
name string
267+
bare string
268+
folders int
269+
wantErr bool
270+
}{
271+
{name: "targets cover the folders", bare: withTargets, folders: demo.MaxShardDirs},
272+
{name: "targets do not cover the folders", bare: withTargets, folders: demo.MaxShardDirs + 1, wantErr: true},
273+
{name: "no targets, any folder count", bare: plain, folders: 50},
274+
}
275+
for _, tt := range tests {
276+
t.Run(tt.name, func(t *testing.T) {
277+
err := checkFoldersHaveTargets(ctx, git, tt.bare, "main", tt.folders)
278+
if tt.wantErr {
279+
require.Error(t, err)
280+
return
281+
}
282+
require.NoError(t, err)
283+
})
284+
}
285+
}

‎service/submitqueue/demo/workload.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ import (
2323
// Five to ten shards produce both shared-directory and independent changes.
2424
const (
2525
minShardDirs = 5
26-
maxShardDirs = 10
26+
// MaxShardDirs is also how many shard folders have TARGET files in the Tango demo sandbox.
27+
MaxShardDirs = 10
2728
)
2829

2930
func resolveFolders(tag string, configured int) int {
3031
if configured > 0 {
3132
return configured
3233
}
3334
sum := sha256.Sum256([]byte("folders#" + tag))
34-
return minShardDirs + int(sum[0])%(maxShardDirs-minShardDirs+1)
35+
return minShardDirs + int(sum[0])%(MaxShardDirs-minShardDirs+1)
3536
}
3637

3738
func changeShard(tag string, folders, change int) string {

‎service/submitqueue/demo/workload_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestResolveFolders(t *testing.T) {
8686
for minute := range 60 {
8787
folders := resolveFolders(fmt.Sprintf("0810-12%02d", minute), 0)
8888
assert.GreaterOrEqual(t, folders, minShardDirs)
89-
assert.LessOrEqual(t, folders, maxShardDirs)
89+
assert.LessOrEqual(t, folders, MaxShardDirs)
9090
seen[folders] = struct{}{}
9191
}
9292
assert.Greater(t, len(seen), 1, "runs must not all pick the same number")

0 commit comments

Comments
 (0)