Skip to content

Commit b69adb2

Browse files
kevinlnewclaude
andcommitted
feat(runway): add example orchestrator wiring and Makefile targets
Consumer-only wiring with noop VCS factory, docker-compose (mysql-queue only, no app DB), and Makefile targets for build/start/stop. Runway is stateless — no application database needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d3f8423 commit b69adb2

5 files changed

Lines changed: 337 additions & 2 deletions

File tree

‎Makefile‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ STOVEPIPE_STACK_COMPOSE_FILE = example/stovepipe/docker-compose.yml
2020
# Fixed project name for local manual testing (tests use unique random names)
2121
STOVEPIPE_LOCAL_PROJECT = stovepipe
2222

23+
# Runway compose files
24+
RUNWAY_ORCHESTRATOR_COMPOSE_FILE = example/runway/orchestrator/server/docker-compose.yml
25+
26+
# Fixed project name for local manual testing (tests use unique random names)
27+
RUNWAY_LOCAL_PROJECT = runway
28+
2329
# yamlfmt version for YAML formatting (override with: make fmt YAMLFMT_VERSION=v0.16.0)
2430
YAMLFMT_VERSION ?= v0.16.0
2531

@@ -47,7 +53,7 @@ define assert_clean
4753
fi
4854
endef
4955

50-
.PHONY: build build-all-linux build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-gateway-linux build-stovepipe-orchestrator-linux check-gazelle check-mocks check-tidy clean clean-proto deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-fmt lint-license local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-init-stovepipe-queue-schema local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-gateway-start local-stovepipe-orchestrator-start local-stovepipe-start mocks proto query-deps query-targets run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe-gateway run-client-stovepipe-orchestrator run-queue-admin test test-no-cache tidy tidy-bazel tidy-go help
56+
.PHONY: build build-all-linux build-runway-orchestrator-linux build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-gateway-linux build-stovepipe-orchestrator-linux check-gazelle check-mocks check-tidy clean clean-proto deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-queue-schema local-init-submitqueue-schemas local-runway-orchestrator-start local-stop local-stovepipe-gateway-start local-stovepipe-orchestrator-start local-stovepipe-start local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start mocks proto query-deps query-targets run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe-gateway run-client-stovepipe-orchestrator run-queue-admin test test-no-cache tidy tidy-bazel tidy-go help
5157

5258

5359
build: ## Build all services and examples
@@ -56,7 +62,7 @@ build: ## Build all services and examples
5662
@echo "Build complete!"
5763

5864
# Build Linux binaries required for Docker containers
59-
build-all-linux: build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-gateway-linux build-stovepipe-orchestrator-linux ## Build all Linux binaries for Docker
65+
build-all-linux: build-runway-orchestrator-linux build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-stovepipe-gateway-linux build-stovepipe-orchestrator-linux ## Build all Linux binaries for Docker
6066
@echo "All Linux binaries ready for Docker"
6167

6268
build-submitqueue-gateway-linux: ## Build Gateway Linux binary for Docker
@@ -75,6 +81,14 @@ build-submitqueue-orchestrator-linux: ## Build Orchestrator Linux binary for Doc
7581
cp -f bazel-bin/example/submitqueue/orchestrator/server/orchestrator .docker-bin/orchestrator
7682
@echo "Orchestrator Linux binary ready at .docker-bin/orchestrator"
7783

84+
build-runway-orchestrator-linux: ## Build Runway orchestrator Linux binary for Docker
85+
@echo "Building Runway orchestrator Linux binary for Docker..."
86+
@$(BAZEL) build --platforms=@rules_go//go/toolchain:linux_amd64 //example/runway/orchestrator/server:orchestrator
87+
@mkdir -p .docker-bin
88+
@cp -f bazel-bin/example/runway/orchestrator/server/orchestrator_/orchestrator .docker-bin/runway-orchestrator 2>/dev/null || \
89+
cp -f bazel-bin/example/runway/orchestrator/server/orchestrator .docker-bin/runway-orchestrator
90+
@echo "Runway orchestrator Linux binary ready at .docker-bin/runway-orchestrator"
91+
7892
build-stovepipe-gateway-linux: ## Build Stovepipe gateway Linux binary for Docker
7993
@echo "Building Stovepipe gateway Linux binary for Docker..."
8094
@$(BAZEL) build --platforms=@rules_go//go/toolchain:linux_amd64 //example/stovepipe/gateway/server:gateway
@@ -209,6 +223,14 @@ local-init-submitqueue-schemas: ## Manually apply all database schemas
209223
done
210224
@echo "✅ All schemas applied successfully"
211225

226+
local-init-runway-queue-schema: ## Apply queue schema only (mysql-queue) for Runway compose stacks
227+
@echo "Applying queue schema to mysql-queue (Runway; stateless — no app schema)..."
228+
@for file in platform/extension/messagequeue/mysql/schema/*.sql; do \
229+
echo " - Applying $$(basename $$file)..."; \
230+
docker exec -i $(RUNWAY_LOCAL_PROJECT)-mysql-queue-1 mysql -uroot -proot submitqueue < $$file 2>&1 | grep -v "Using a password" || true; \
231+
done
232+
@echo "Runway queue schema applied successfully"
233+
212234
local-init-stovepipe-queue-schema: ## Apply queue schema only (mysql-queue) for Stovepipe compose stacks
213235
@echo "Applying queue schema to mysql-queue (Stovepipe; no app storage/counter schema yet)..."
214236
@for file in platform/extension/messagequeue/mysql/schema/*.sql; do \
@@ -278,10 +300,23 @@ local-submitqueue-start: build-all-linux ## Start full stack (Gateway + Orchestr
278300
@echo ""
279301
@make local-submitqueue-ps
280302

303+
local-runway-orchestrator-start: build-runway-orchestrator-linux ## Start Runway orchestrator locally (orchestrator + MySQL queue)
304+
@echo "Starting Runway orchestrator with compose..."
305+
@$(COMPOSE) -f $(RUNWAY_ORCHESTRATOR_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) up -d --build --wait
306+
@echo "Applying queue schema to mysql-queue..."
307+
@$(MAKE) -s local-init-runway-queue-schema
308+
@echo ""
309+
@echo "Runway orchestrator is running!"
310+
@echo ""
311+
@$(COMPOSE) -f $(RUNWAY_ORCHESTRATOR_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) ps
312+
@echo ""
313+
@echo "MySQL Queue port: $$(docker port $(RUNWAY_LOCAL_PROJECT)-mysql-queue-1 3306 2>/dev/null | cut -d: -f2 || echo 'unknown')"
314+
281315
local-stop: ## Stop all services (keep data)
282316
@echo "Stopping all services..."
283317
@$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down
284318
@$(COMPOSE) -f $(STOVEPIPE_STACK_COMPOSE_FILE) -p $(STOVEPIPE_LOCAL_PROJECT) down
319+
@$(COMPOSE) -f $(RUNWAY_ORCHESTRATOR_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) down
285320
@echo "Services stopped. Data volumes preserved."
286321

287322
local-stovepipe-logs: ## View logs from all running Stovepipe services
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "server_lib",
5+
srcs = ["main.go"],
6+
importpath = "github.com/uber/submitqueue/example/runway/orchestrator/server",
7+
visibility = ["//visibility:private"],
8+
deps = [
9+
"//platform/consumer",
10+
"//platform/errs",
11+
"//platform/errs/generic",
12+
"//platform/errs/mysql",
13+
"//platform/extension/messagequeue",
14+
"//platform/extension/messagequeue/mysql",
15+
"//runway/core/topickey",
16+
"//runway/extension/vcs",
17+
"//runway/extension/vcs/noop",
18+
"//runway/orchestrator/controller/merge",
19+
"//runway/orchestrator/controller/mergeconflictcheck",
20+
"@com_github_go_sql_driver_mysql//:mysql",
21+
"@com_github_uber_go_tally//:tally",
22+
"@org_uber_go_zap//:zap",
23+
],
24+
)
25+
26+
go_binary(
27+
name = "orchestrator",
28+
embed = [":server_lib"],
29+
visibility = ["//visibility:public"],
30+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM debian:bookworm-slim
2+
3+
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
4+
WORKDIR /root/
5+
6+
# Built via: make build-runway-orchestrator-linux
7+
COPY .docker-bin/runway-orchestrator ./orchestrator
8+
9+
CMD ["./orchestrator"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Docker Compose for Runway orchestrator manual testing
2+
#
3+
#
4+
# IMPORTANT: Before running compose, build the Linux binary:
5+
# make build-runway-orchestrator-linux
6+
# OR
7+
# bazel build --platforms=@rules_go//go/toolchain:linux_amd64 //example/runway/orchestrator/server:orchestrator
8+
#
9+
# Quick start:
10+
# make local-runway-orchestrator-start
11+
#
12+
# After `up`, only the queue schema is applied (`local-init-runway-queue-schema`).
13+
# Runway is stateless — no application database is needed.
14+
15+
services:
16+
# Queue Database - Messaging infrastructure (messages, offsets, partition leases)
17+
mysql-queue:
18+
image: mysql:8.0
19+
environment:
20+
MYSQL_ROOT_PASSWORD: root
21+
MYSQL_DATABASE: submitqueue
22+
ports:
23+
- "3306" # Random ephemeral port to avoid conflicts
24+
healthcheck:
25+
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
26+
interval: 5s
27+
timeout: 5s
28+
retries: 10
29+
30+
orchestrator-service:
31+
build:
32+
context: ${REPO_ROOT}
33+
dockerfile: example/runway/orchestrator/server/Dockerfile
34+
environment:
35+
# Queue infrastructure connection (runway is stateless — no app DB)
36+
- QUEUE_MYSQL_DSN=root:root@tcp(mysql-queue:3306)/submitqueue?parseTime=true
37+
- HOSTNAME=orchestrator-dev
38+
depends_on:
39+
mysql-queue:
40+
condition: service_healthy
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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 main
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
"errors"
21+
"fmt"
22+
"os"
23+
"os/signal"
24+
"sync"
25+
"syscall"
26+
"time"
27+
28+
_ "github.com/go-sql-driver/mysql"
29+
"github.com/uber-go/tally"
30+
"github.com/uber/submitqueue/platform/consumer"
31+
"github.com/uber/submitqueue/platform/errs"
32+
genericerrs "github.com/uber/submitqueue/platform/errs/generic"
33+
mysqlerrs "github.com/uber/submitqueue/platform/errs/mysql"
34+
extqueue "github.com/uber/submitqueue/platform/extension/messagequeue"
35+
queueMySQL "github.com/uber/submitqueue/platform/extension/messagequeue/mysql"
36+
"github.com/uber/submitqueue/runway/core/topickey"
37+
"github.com/uber/submitqueue/runway/extension/vcs"
38+
"github.com/uber/submitqueue/runway/extension/vcs/noop"
39+
"github.com/uber/submitqueue/runway/orchestrator/controller/merge"
40+
"github.com/uber/submitqueue/runway/orchestrator/controller/mergeconflictcheck"
41+
"go.uber.org/zap"
42+
)
43+
44+
// noopVCSFactory adapts the noop VCS into the vcs.Factory interface.
45+
type noopVCSFactory struct {
46+
instance *noop.VCS
47+
}
48+
49+
func (f *noopVCSFactory) For(_ vcs.Config) (vcs.VCS, error) {
50+
return f.instance, nil
51+
}
52+
53+
func main() {
54+
code := 0
55+
if err := run(); err != nil {
56+
if errors.Is(err, context.Canceled) {
57+
fmt.Println("Runway orchestrator server stopped by signal")
58+
code = 128 + int(syscall.SIGTERM)
59+
} else {
60+
fmt.Fprintf(os.Stderr, "Runway orchestrator server failure: %v\n", err)
61+
code = 1
62+
}
63+
}
64+
os.Exit(code)
65+
}
66+
67+
func run() error {
68+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
69+
defer cancel()
70+
71+
logger, err := zap.NewDevelopment()
72+
if err != nil {
73+
return fmt.Errorf("failed to create logger: %w", err)
74+
}
75+
defer logger.Sync()
76+
77+
scope := tally.NewTestScope("runway_orchestrator", nil)
78+
metricsStopCh := make(chan any, 1)
79+
metricsWgDone := sync.WaitGroup{}
80+
metricsWgDone.Add(1)
81+
go func() {
82+
defer metricsWgDone.Done()
83+
84+
ticker := time.NewTicker(10 * time.Second)
85+
defer ticker.Stop()
86+
87+
for {
88+
select {
89+
case <-metricsStopCh:
90+
return
91+
case <-ticker.C:
92+
snapshot := scope.Snapshot()
93+
logger.Info("metrics snapshot",
94+
zap.Any("counters", snapshot.Counters()),
95+
zap.Any("gauges", snapshot.Gauges()),
96+
zap.Any("timers", snapshot.Timers()),
97+
)
98+
}
99+
}
100+
}()
101+
102+
defer func() {
103+
close(metricsStopCh)
104+
metricsWgDone.Wait()
105+
}()
106+
107+
queueDSN := os.Getenv("QUEUE_MYSQL_DSN")
108+
if queueDSN == "" {
109+
return fmt.Errorf("QUEUE_MYSQL_DSN environment variable is required")
110+
}
111+
queueDB, err := sql.Open("mysql", queueDSN)
112+
if err != nil {
113+
return fmt.Errorf("failed to open queue database: %w", err)
114+
}
115+
defer queueDB.Close()
116+
117+
mysqlQueue, err := queueMySQL.NewQueue(queueMySQL.Params{
118+
DB: queueDB,
119+
Logger: logger,
120+
MetricsScope: scope.SubScope("queue"),
121+
})
122+
if err != nil {
123+
return fmt.Errorf("failed to create queue: %w", err)
124+
}
125+
defer mysqlQueue.Close()
126+
127+
logger.Info("initialized queue", zap.String("dsn", queueDSN))
128+
129+
subscriberName := os.Getenv("HOSTNAME")
130+
if subscriberName == "" {
131+
subscriberName = fmt.Sprintf("runway-orchestrator-%d", time.Now().Unix())
132+
}
133+
134+
registry, err := newTopicRegistry(mysqlQueue, subscriberName)
135+
if err != nil {
136+
return fmt.Errorf("failed to create topic registry: %w", err)
137+
}
138+
139+
vcsFactory := &noopVCSFactory{instance: noop.New()}
140+
141+
primaryConsumer := consumer.New(logger.Sugar(), scope.SubScope("consumer"), registry,
142+
errs.NewClassifierProcessor(
143+
genericerrs.Classifier,
144+
mysqlerrs.Classifier,
145+
),
146+
)
147+
148+
checkController := mergeconflictcheck.NewController(mergeconflictcheck.Params{
149+
Logger: logger.Sugar(),
150+
Scope: scope,
151+
Registry: registry,
152+
VCSFactory: vcsFactory,
153+
TopicKey: topickey.TopicKeyMergeConflictCheck,
154+
ConsumerGroup: "runway-merge-conflict-check",
155+
})
156+
if err := primaryConsumer.Register(checkController); err != nil {
157+
return fmt.Errorf("failed to register merge-conflict-check controller: %w", err)
158+
}
159+
160+
mergeController := merge.NewController(merge.Params{
161+
Logger: logger.Sugar(),
162+
Scope: scope,
163+
Registry: registry,
164+
VCSFactory: vcsFactory,
165+
TopicKey: topickey.TopicKeyMerge,
166+
ConsumerGroup: "runway-merge",
167+
})
168+
if err := primaryConsumer.Register(mergeController); err != nil {
169+
return fmt.Errorf("failed to register merge controller: %w", err)
170+
}
171+
172+
logger.Info("controllers registered", zap.Int("primary", 2))
173+
174+
if err := primaryConsumer.Start(ctx); err != nil {
175+
return fmt.Errorf("failed to start primary consumer: %w", err)
176+
}
177+
178+
fmt.Println("Runway orchestrator server is running (consumer-only, no gRPC)")
179+
fmt.Println("Press Ctrl+C to stop, or send a SIGTERM.")
180+
181+
<-ctx.Done()
182+
fmt.Println("Shutting down runway orchestrator server due to interruption signal...")
183+
184+
stopErr := primaryConsumer.Stop(30000)
185+
if stopErr != nil {
186+
return fmt.Errorf("failed to stop consumer: %w", stopErr)
187+
}
188+
189+
return ctx.Err()
190+
}
191+
192+
func newTopicRegistry(q extqueue.Queue, subscriberName string) (consumer.TopicRegistry, error) {
193+
return consumer.NewTopicRegistry([]consumer.TopicConfig{
194+
{
195+
Key: topickey.TopicKeyMergeConflictCheck,
196+
Name: "merge-conflict-checker",
197+
Queue: q,
198+
Subscription: extqueue.DefaultSubscriptionConfig(
199+
subscriberName, "runway-merge-conflict-check",
200+
),
201+
},
202+
{
203+
Key: topickey.TopicKeyMerge,
204+
Name: "merger",
205+
Queue: q,
206+
Subscription: extqueue.DefaultSubscriptionConfig(
207+
subscriberName, "runway-merge",
208+
),
209+
},
210+
{
211+
Key: topickey.TopicKeyMergeConflictCheckSignal,
212+
Name: "merge-conflict-checker-signal",
213+
Queue: q,
214+
},
215+
{
216+
Key: topickey.TopicKeyMergeSignal,
217+
Name: "merger-signal",
218+
Queue: q,
219+
},
220+
})
221+
}

0 commit comments

Comments
 (0)