Skip to content

Commit 0932326

Browse files
kevinlnewclaude
andcommitted
feat(runway): add proto definitions, Ping controller, and gRPC server (#6)
Add a RunwayOrchestrator gRPC service with a Ping health-check endpoint, mirroring the stovepipe/submitqueue pattern. Wire the gRPC server into the example orchestrator alongside the consumer. This is an optional accessory — the core service works without it. Also fixes the workflow.md diagram box-drawing characters that rendered as strikethroughs in rich diffs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e4cfa4b commit 0932326

14 files changed

Lines changed: 964 additions & 6 deletions

File tree

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ load("@gazelle//:def.bzl", "gazelle")
77
# gazelle:exclude .claude
88

99
# Resolve protobuf import ambiguities - use the actual protopb packages, not the proto aliases
10+
# gazelle:resolve go github.com/uber/submitqueue/runway/orchestrator/protopb //runway/orchestrator/protopb
1011
# gazelle:resolve go github.com/uber/submitqueue/submitqueue/gateway/protopb //submitqueue/gateway/protopb
1112
# gazelle:resolve go github.com/uber/submitqueue/submitqueue/orchestrator/protopb //submitqueue/orchestrator/protopb
1213
# gazelle:resolve go github.com/uber/submitqueue/stovepipe/gateway/protopb //stovepipe/gateway/protopb

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ GOIMPORTS_VERSION ?= v0.33.0
3535
# Proto packages: <domain>/<service> dirs whose protopb/ holds the generated
3636
# stubs. Each is generated by Bazel into bazel-bin/tool/proto/<domain>_<service>/
3737
# (the out_dir convention in tool/proto/BUILD.bazel) and copied back here.
38-
PROTO_PACKAGES = submitqueue/gateway submitqueue/orchestrator stovepipe/gateway stovepipe/orchestrator
38+
PROTO_PACKAGES = runway/orchestrator submitqueue/gateway submitqueue/orchestrator stovepipe/gateway stovepipe/orchestrator
3939

4040
# Set REPO_ROOT for docker-compose
4141
export REPO_ROOT := $(shell pwd)

example/runway/orchestrator/server/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ go_library(
1515
"//runway/core/topickey",
1616
"//runway/extension/vcs",
1717
"//runway/extension/vcs/noop",
18+
"//runway/orchestrator/controller",
1819
"//runway/orchestrator/controller/check",
1920
"//runway/orchestrator/controller/land",
21+
"//runway/orchestrator/protopb",
2022
"@com_github_go_sql_driver_mysql//:mysql",
2123
"@com_github_uber_go_tally//:tally",
24+
"@org_golang_google_grpc//:grpc",
25+
"@org_golang_google_grpc//reflection",
2226
"@org_uber_go_zap//:zap",
2327
],
2428
)

example/runway/orchestrator/server/main.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"database/sql"
2020
"errors"
2121
"fmt"
22+
"net"
2223
"os"
2324
"os/signal"
2425
"sync"
@@ -28,6 +29,8 @@ import (
2829
_ "github.com/go-sql-driver/mysql"
2930
"github.com/uber-go/tally"
3031
"go.uber.org/zap"
32+
"google.golang.org/grpc"
33+
"google.golang.org/grpc/reflection"
3134

3235
"github.com/uber/submitqueue/core/consumer"
3336
"github.com/uber/submitqueue/core/errs"
@@ -38,8 +41,10 @@ import (
3841
"github.com/uber/submitqueue/runway/core/topickey"
3942
"github.com/uber/submitqueue/runway/extension/vcs"
4043
"github.com/uber/submitqueue/runway/extension/vcs/noop"
44+
"github.com/uber/submitqueue/runway/orchestrator/controller"
4145
"github.com/uber/submitqueue/runway/orchestrator/controller/check"
4246
"github.com/uber/submitqueue/runway/orchestrator/controller/land"
47+
pb "github.com/uber/submitqueue/runway/orchestrator/protopb"
4348
)
4449

4550
func main() {
@@ -56,6 +61,17 @@ func main() {
5661
os.Exit(code)
5762
}
5863

64+
// OrchestratorServer wraps the controller and implements the gRPC service interface.
65+
type OrchestratorServer struct {
66+
pb.UnimplementedRunwayOrchestratorServer
67+
pingController *controller.PingController
68+
}
69+
70+
// Ping delegates to the controller.
71+
func (s *OrchestratorServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error) {
72+
return s.pingController.Ping(ctx, req)
73+
}
74+
5975
// noopVCSFactory returns the noop VCS for any configuration.
6076
type noopVCSFactory struct{}
6177

@@ -153,16 +169,48 @@ func run() error {
153169
return fmt.Errorf("failed to start consumer: %w", err)
154170
}
155171

156-
fmt.Println("Runway orchestrator is running. Press Ctrl+C to stop, or send a SIGTERM.")
172+
// gRPC server for health checks (Ping).
173+
grpcServer := grpc.NewServer()
174+
pingController := controller.NewPingController(logger, scope)
175+
srv := &OrchestratorServer{pingController: pingController}
176+
pb.RegisterRunwayOrchestratorServer(grpcServer, srv)
177+
reflection.Register(grpcServer)
178+
179+
port := os.Getenv("PORT")
180+
if port == "" {
181+
port = ":8085"
182+
}
183+
listener, err := net.Listen("tcp", port)
184+
if err != nil {
185+
return fmt.Errorf("failed to listen on port %s: %w", port, err)
186+
}
187+
188+
fmt.Printf("Runway orchestrator is running on %s\n", port)
189+
fmt.Println("Press Ctrl+C to stop, or send a SIGTERM.")
157190

158-
<-ctx.Done()
159-
fmt.Println("Shutting down runway orchestrator...")
191+
serverErrCh := make(chan error, 1)
192+
go func() {
193+
serverErrCh <- grpcServer.Serve(listener)
194+
}()
160195

161-
err = ctx.Err()
196+
var serverErr error
197+
select {
198+
case <-ctx.Done():
199+
fmt.Println("Shutting down runway orchestrator...")
200+
err = ctx.Err()
201+
grpcServer.GracefulStop()
202+
serverErr = <-serverErrCh
203+
case serverErr = <-serverErrCh:
204+
fmt.Println("Shutting down runway orchestrator due to gRPC server error...")
205+
}
206+
207+
if serverErr != nil {
208+
err = fmt.Errorf("gRPC server exited with error: %w", serverErr)
209+
}
162210

163211
stopErr := primaryConsumer.Stop(30000)
164212
if stopErr != nil {
165-
err = fmt.Errorf("failed to stop consumer: %w", stopErr)
213+
err = errors.Join(err, fmt.Errorf("failed to stop consumer: %w", stopErr))
166214
}
167215

168216
return err
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "controller",
5+
srcs = ["ping.go"],
6+
importpath = "github.com/uber/submitqueue/runway/orchestrator/controller",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//core/metrics",
10+
"//runway/orchestrator/protopb",
11+
"@com_github_uber_go_tally//:tally",
12+
"@org_uber_go_zap//:zap",
13+
],
14+
)
15+
16+
go_test(
17+
name = "controller_test",
18+
srcs = ["ping_test.go"],
19+
embed = [":controller"],
20+
deps = [
21+
"//runway/orchestrator/protopb",
22+
"@com_github_stretchr_testify//assert",
23+
"@com_github_stretchr_testify//require",
24+
"@com_github_uber_go_tally//:tally",
25+
"@org_uber_go_zap//:zap",
26+
],
27+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 controller
16+
17+
import (
18+
"context"
19+
"os"
20+
"time"
21+
22+
"github.com/uber-go/tally"
23+
"github.com/uber/submitqueue/core/metrics"
24+
pb "github.com/uber/submitqueue/runway/orchestrator/protopb"
25+
"go.uber.org/zap"
26+
)
27+
28+
// PingController handles ping business logic for the Runway orchestrator.
29+
type PingController struct {
30+
logger *zap.Logger
31+
metricsScope tally.Scope
32+
}
33+
34+
// NewPingController creates a new instance of the Runway orchestrator ping controller.
35+
func NewPingController(logger *zap.Logger, scope tally.Scope) *PingController {
36+
return &PingController{
37+
logger: logger,
38+
metricsScope: scope,
39+
}
40+
}
41+
42+
// Ping handles the ping request and returns a response.
43+
func (c *PingController) Ping(ctx context.Context, req *pb.PingRequest) (resp *pb.PingResponse, retErr error) {
44+
const opName = "ping"
45+
46+
op := metrics.Begin(c.metricsScope, opName)
47+
defer func() { op.Complete(retErr) }()
48+
49+
message := "pong!"
50+
isEcho := false
51+
if req.Message != "" {
52+
message = "echo: " + req.Message
53+
isEcho = true
54+
metrics.NamedCounter(c.metricsScope, opName, "echo_requests", 1)
55+
}
56+
57+
hostname, _ := os.Hostname()
58+
59+
c.logger.Info("ping request received",
60+
zap.String("message", req.Message),
61+
zap.Bool("is_echo", isEcho),
62+
zap.String("hostname", hostname),
63+
)
64+
65+
return &pb.PingResponse{
66+
Message: message,
67+
ServiceName: "runway-orchestrator",
68+
Timestamp: time.Now().Unix(),
69+
Hostname: hostname,
70+
}, nil
71+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 controller
16+
17+
import (
18+
"context"
19+
"testing"
20+
"time"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
"github.com/uber-go/tally"
25+
pb "github.com/uber/submitqueue/runway/orchestrator/protopb"
26+
"go.uber.org/zap"
27+
)
28+
29+
func TestNewPingController(t *testing.T) {
30+
ctrl := NewPingController(zap.NewNop(), tally.NoopScope)
31+
require.NotNil(t, ctrl)
32+
}
33+
34+
func TestPing_DefaultMessage(t *testing.T) {
35+
ctrl := NewPingController(zap.NewNop(), tally.NoopScope)
36+
ctx := context.Background()
37+
38+
req := &pb.PingRequest{}
39+
resp, err := ctrl.Ping(ctx, req)
40+
41+
require.NoError(t, err)
42+
assert.Equal(t, "pong!", resp.Message)
43+
}
44+
45+
func TestPing_ServiceName(t *testing.T) {
46+
ctrl := NewPingController(zap.NewNop(), tally.NoopScope)
47+
ctx := context.Background()
48+
49+
req := &pb.PingRequest{}
50+
resp, err := ctrl.Ping(ctx, req)
51+
52+
require.NoError(t, err)
53+
assert.Equal(t, "runway-orchestrator", resp.ServiceName)
54+
}
55+
56+
func TestPing_Timestamp(t *testing.T) {
57+
ctrl := NewPingController(zap.NewNop(), tally.NoopScope)
58+
ctx := context.Background()
59+
60+
before := time.Now().Unix()
61+
req := &pb.PingRequest{}
62+
resp, err := ctrl.Ping(ctx, req)
63+
after := time.Now().Unix()
64+
65+
require.NoError(t, err)
66+
assert.GreaterOrEqual(t, resp.Timestamp, before)
67+
assert.LessOrEqual(t, resp.Timestamp, after)
68+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
load("@rules_go//proto:def.bzl", "go_proto_library")
3+
load("@rules_proto//proto:defs.bzl", "proto_library")
4+
5+
exports_files(
6+
["orchestrator.proto"],
7+
visibility = ["//tool/proto:__pkg__"],
8+
)
9+
10+
proto_library(
11+
name = "orchestratorpb_proto",
12+
srcs = ["orchestrator.proto"],
13+
visibility = ["//visibility:public"],
14+
)
15+
16+
# keep
17+
go_proto_library(
18+
name = "orchestratorpb_go_proto",
19+
compilers = [
20+
"@rules_go//proto:go_proto",
21+
"@rules_go//proto:go_grpc_v2",
22+
],
23+
importpath = "github.com/uber/submitqueue/runway/orchestrator/proto",
24+
proto = ":orchestratorpb_proto",
25+
visibility = ["//visibility:public"],
26+
)
27+
28+
go_library(
29+
name = "proto",
30+
embed = [":orchestratorpb_go_proto"],
31+
importpath = "github.com/uber/submitqueue/runway/orchestrator/proto",
32+
visibility = ["//visibility:public"],
33+
)
34+
35+
go_library(
36+
name = "protopb",
37+
embed = [":orchestratorpb_go_proto"],
38+
importpath = "github.com/uber/submitqueue/runway/orchestrator/protopb",
39+
visibility = ["//visibility:public"],
40+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
syntax = "proto3";
16+
17+
package uber.submitqueue.runway.orchestrator;
18+
19+
option go_package = "github.com/uber/submitqueue/runway/orchestrator/protopb";
20+
option java_multiple_files = true;
21+
option java_outer_classname = "OrchestratorProto";
22+
option java_package = "com.uber.submitqueue.runway.orchestrator";
23+
24+
// PingRequest is the request for the Ping method
25+
message PingRequest {
26+
// Optional message to include in the ping
27+
string message = 1;
28+
}
29+
30+
// PingResponse is the response for the Ping method
31+
message PingResponse {
32+
// The response message
33+
string message = 1;
34+
// The service name that handled the request
35+
string service_name = 2;
36+
// Timestamp of when the ping was received
37+
int64 timestamp = 3;
38+
// Hostname of the server that handled the request
39+
string hostname = 4;
40+
}
41+
42+
// RunwayOrchestrator provides the Runway orchestrator API.
43+
service RunwayOrchestrator {
44+
// Ping returns a response indicating the service is alive
45+
rpc Ping(PingRequest) returns (PingResponse) {}
46+
}

0 commit comments

Comments
 (0)