Skip to content

Commit 2362c66

Browse files
authored
Add support for a ping command to pipe clients. (#316)
- Add a simple ping protocol to pipe clients conformance service - Default pipe to use base64 encoding instead of JSON
1 parent 0d5e40d commit 2362c66

File tree

6 files changed

+145
-43
lines changed

6 files changed

+145
-43
lines changed

tests/simple/simple_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ var (
4949
flagSkipCheck bool
5050
flagSkipTests stringArray
5151
flagPipe bool
52+
flagPipePings bool
5253
flagPipeBase64 bool
5354
rc *runConfig
5455
)
@@ -62,7 +63,8 @@ func init() {
6263
flag.BoolVar(&flagSkipCheck, "skip_check", false, "force skipping the check phase")
6364
flag.Var(&flagSkipTests, "skip_test", "name(s) of tests to skip. can be set multiple times. to skip the following tests: f1/s1/t1, f1/s1/t2, f1/s2/*, f2/s3/t3, you give the arguments --skip_test=f1/s1/t1,t2;s2 --skip_test=f2/s3/t3")
6465
flag.BoolVar(&flagPipe, "pipe", false, "Use pipes instead of gRPC")
65-
flag.BoolVar(&flagPipeBase64, "pipe_base64", false, "Use base64 encoded wire format proto in pipes (default JSON).")
66+
flag.BoolVar(&flagPipeBase64, "pipe_base64", true, "Use base64 encoded wire format proto in pipes (if disabled, use JSON).")
67+
flag.BoolVar(&flagPipePings, "pipe_pings", false, "Enable pinging pipe client to subprocess status.")
6668

6769
flag.Parse()
6870
}
@@ -103,7 +105,7 @@ func initRunConfig() (*runConfig, error) {
103105
var cli celrpc.ConfClient
104106
var err error
105107
if flagPipe {
106-
cli, err = celrpc.NewPipeClient(cmd, flagPipeBase64)
108+
cli, err = celrpc.NewPipeClient(cmd, flagPipeBase64, flagPipePings)
107109
} else {
108110
cli, err = celrpc.NewGrpcClient(cmd)
109111
}

tools/celrpc/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ go_library(
1616
"@org_golang_google_grpc//reflection:go_default_library",
1717
"@org_golang_google_protobuf//encoding/protojson:go_default_library",
1818
"@org_golang_google_protobuf//proto:go_default_library",
19+
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
1920
],
2021
)
2122

tools/celrpc/celrpc.go

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bufio"
66
"context"
77
"encoding/base64"
8+
"errors"
89
"fmt"
910
"io"
1011
"log"
@@ -17,6 +18,7 @@ import (
1718
"google.golang.org/grpc/reflection"
1819
"google.golang.org/protobuf/encoding/protojson"
1920
"google.golang.org/protobuf/proto"
21+
"google.golang.org/protobuf/types/known/emptypb"
2022

2123
confpb "google.golang.org/genproto/googleapis/api/expr/conformance/v1alpha1"
2224
)
@@ -38,15 +40,18 @@ type grpcConfClient struct {
3840
}
3941

4042
// pipe conformance client uses the following protocol:
41-
// * two lines are sent over input
42-
// * first input line is "parse", "check", or "eval"
43-
// * second input line is JSON of the corresponding request
44-
// * one output line is expected, repeat again.
43+
// - two lines are sent over input
44+
// - first input line is "parse", "check", "ping", or "eval"
45+
// - second input line is encoded request message
46+
// - one output line is expected, repeat again.
4547
type pipeConfClient struct {
46-
cmd *exec.Cmd
47-
stdOut *bufio.Reader
48-
stdIn io.Writer
49-
useBase64 bool
48+
binary string
49+
cmd_args []string
50+
cmd *exec.Cmd
51+
stdOut *bufio.Reader
52+
stdIn io.Writer
53+
useBase64 bool
54+
pingsEnabled bool
5055
}
5156

5257
// NewGrpcClient creates a new gRPC ConformanceService client. A server binary
@@ -122,60 +127,62 @@ func ExampleNewGrpcClient() {
122127
// method returns a non-nil error.
123128
//
124129
// base64Encode enables base64Encoded messages (b64encode(Any.serializeToString))
125-
func NewPipeClient(serverCmd string, base64Encode bool) (ConfClient, error) {
130+
// pingsEnabled enables pinging between reqests to test subprocess health
131+
func NewPipeClient(serverCmd string, base64Encode bool, pingsEnabled bool) (ConfClient, error) {
126132
c := pipeConfClient{
127-
useBase64: base64Encode,
133+
useBase64: base64Encode,
134+
pingsEnabled: pingsEnabled,
128135
}
129136

130137
fields := strings.Fields(serverCmd)
131138
if len(fields) < 1 {
132139
return &c, fmt.Errorf("server cmd '%s' invalid", serverCmd)
133140
}
134-
cmd := exec.Command(fields[0], fields[1:]...)
141+
c.binary = fields[0]
142+
c.cmd_args = fields[1:]
143+
144+
return &c, c.reset()
145+
}
146+
147+
// reset restarts the conformance server piped implementation.
148+
func (c *pipeConfClient) reset() error {
149+
if c.binary == "" {
150+
return errors.New("reset on invalid pipe service configuration")
151+
}
152+
cmd := exec.Command(c.binary, c.cmd_args...)
135153
out, err := cmd.StdoutPipe()
136154
if err != nil {
137-
return &c, err
155+
return err
138156
}
139157
c.stdIn, err = cmd.StdinPipe()
140158
if err != nil {
141-
return &c, err
159+
return err
142160
}
143161
cmd.Stderr = os.Stderr // share our error stream
144162

145163
err = cmd.Start()
146164
if err != nil {
147-
return &c, err
165+
return err
148166
}
149167
// Only assign cmd for stopping if it has successfully started.
150168
c.cmd = cmd
151169
c.stdOut = bufio.NewReader(out)
152-
return &c, nil
170+
return nil
153171
}
154172

155-
// ExampleNewPipeClient creates a new CEL pipe client using a path to a server binary.
156-
// TODO Run from celrpc_test.go.
157-
func ExampleNewPipeClient() {
158-
c, err := NewPipeClient("/path/to/server/binary", false)
159-
defer c.Shutdown()
160-
if err != nil {
161-
log.Fatal("Couldn't create client")
162-
}
163-
parseRequest := confpb.ParseRequest{
164-
CelSource: "1 + 1",
165-
}
166-
parseResponse, err := c.Parse(context.Background(), &parseRequest)
167-
if err != nil {
168-
log.Fatal("Couldn't parse")
169-
}
170-
parsedExpr := parseResponse.ParsedExpr
171-
evalRequest := confpb.EvalRequest{
172-
ExprKind: &confpb.EvalRequest_ParsedExpr{ParsedExpr: parsedExpr},
173-
}
174-
evalResponse, err := c.Eval(context.Background(), &evalRequest)
175-
if err != nil {
176-
log.Fatal("Couldn't eval")
173+
func (c *pipeConfClient) isAlive() bool {
174+
m := emptypb.Empty{}
175+
err := c.pipeCommand("ping", &m, &m)
176+
return err == nil
177+
}
178+
179+
// checkAlive tests the client process health and restarts it on failure.
180+
func (c *pipeConfClient) checkAlive() error {
181+
if c.isAlive() {
182+
return nil
177183
}
178-
fmt.Printf("1 + 1 is %v\n", evalResponse.Result.GetValue().GetInt64Value())
184+
c.Shutdown()
185+
return c.reset()
179186
}
180187

181188
func (c *pipeConfClient) marshal(in proto.Message) (string, error) {
@@ -203,6 +210,11 @@ func (c *pipeConfClient) unmarshal(encoded string, out proto.Message) error {
203210
}
204211

205212
func (c *pipeConfClient) pipeCommand(cmd string, in proto.Message, out proto.Message) error {
213+
if c.pingsEnabled && cmd != "ping" {
214+
if err := c.checkAlive(); err != nil {
215+
return err
216+
}
217+
}
206218
if _, err := c.stdIn.Write([]byte(cmd + "\n")); err != nil {
207219
return err
208220
}

tools/celrpc/celrpc_test.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func TestPipeParse(t *testing.T) {
3737
if err != nil {
3838
t.Fatalf("error loading bazel runfile path, %v", err)
3939
}
40-
conf, err := NewPipeClient(serverCmd, useBase64)
40+
conf, err := NewPipeClient(serverCmd, useBase64, false /*usePings*/)
4141
defer conf.Shutdown()
4242
if err != nil {
4343
t.Fatalf("error initializing client got %v wanted nil", err)
@@ -69,6 +69,76 @@ func TestPipeParse(t *testing.T) {
6969
t.Errorf("Issues: got %v expected none", resp.Issues)
7070
}
7171

72+
if resp.GetParsedExpr().GetExpr().GetCallExpr().GetFunction() != "_+_" {
73+
t.Errorf("unexpected ast got: %s wanted _+_(1, 1)", resp.GetParsedExpr())
74+
}
75+
})
76+
}
77+
}
78+
79+
func TestPipeCrashRecover(t *testing.T) {
80+
for _, useBase64 := range []bool{false, true} {
81+
t := t
82+
t.Run(fmt.Sprintf("useBase64=%v", useBase64), func(t *testing.T) {
83+
serverCmd, err := bazel.Runfile(serverCmd)
84+
85+
if useBase64 {
86+
serverCmd = fmt.Sprintf("%s %s", serverCmd, serverBase64Flag)
87+
}
88+
89+
if err != nil {
90+
t.Fatalf("error loading bazel runfile path, %v", err)
91+
}
92+
conf, err := NewPipeClient(serverCmd, useBase64, true /*usePings*/)
93+
defer conf.Shutdown()
94+
if err != nil {
95+
t.Fatalf("error initializing client got %v wanted nil", err)
96+
}
97+
var resp *confpb.ParseResponse
98+
r := make(chan *confpb.ParseResponse)
99+
e := make(chan error)
100+
go func() {
101+
resp, err := conf.Parse(context.Background(), &confpb.ParseRequest{
102+
CelSource: "test_crash",
103+
})
104+
e <- err
105+
r <- resp
106+
}()
107+
108+
select {
109+
case <-time.After(2 * time.Second):
110+
err = errors.New("timeout")
111+
case err = <-e:
112+
resp = <-r
113+
}
114+
115+
if err == nil {
116+
t.Fatalf("Expected error from pipe, got nil")
117+
}
118+
119+
go func() {
120+
resp, err := conf.Parse(context.Background(), &confpb.ParseRequest{
121+
CelSource: "1 + 1",
122+
})
123+
e <- err
124+
r <- resp
125+
}()
126+
127+
select {
128+
case <-time.After(2 * time.Second):
129+
err = errors.New("timeout")
130+
case err = <-e:
131+
resp = <-r
132+
}
133+
134+
if err != nil {
135+
t.Fatalf("error from pipe: %v", err)
136+
}
137+
138+
if len(resp.Issues) > 0 {
139+
t.Errorf("Issues: got %v expected none", resp.Issues)
140+
}
141+
72142
if resp.GetParsedExpr().GetExpr().GetCallExpr().GetFunction() != "_+_" {
73143
t.Errorf("unexpected ast got: %s wanted _+_(1, 1)", resp.GetParsedExpr())
74144
}
@@ -90,7 +160,7 @@ func TestPipeEval(t *testing.T) {
90160
if err != nil {
91161
t.Fatalf("error loading bazel runfile path, %v", err)
92162
}
93-
conf, err := NewPipeClient(serverCmd, useBase64)
163+
conf, err := NewPipeClient(serverCmd, useBase64, false /*usePings*/)
94164
defer conf.Shutdown()
95165
if err != nil {
96166
t.Fatalf("error initializing client got %v wanted nil", err)
@@ -140,7 +210,7 @@ func TestPipeCheck(t *testing.T) {
140210
if err != nil {
141211
t.Fatalf("error loading bazel runfile path, %v", err)
142212
}
143-
conf, err := NewPipeClient(serverCmd, useBase64)
213+
conf, err := NewPipeClient(serverCmd, useBase64, false /*usePings*/)
144214
defer conf.Shutdown()
145215
if err != nil {
146216
t.Fatalf("error initializing client got %v wanted nil", err)

tools/celrpc/testpipeimpl/main/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ go_binary(
1212
deps = [
1313
"@org_golang_google_protobuf//encoding/protojson:go_default_library",
1414
"@org_golang_google_protobuf//proto:go_default_library",
15+
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
1516
"@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
1617
"@org_golang_google_genproto_googleapis_api//expr/conformance/v1alpha1:go_default_library",
1718
"@org_golang_google_genproto_googleapis_rpc//status:go_default_library",

tools/celrpc/testpipeimpl/main/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"google.golang.org/grpc/codes"
1616
"google.golang.org/protobuf/encoding/protojson"
1717
"google.golang.org/protobuf/proto"
18+
"google.golang.org/protobuf/types/known/emptypb"
1819

1920
confpb "google.golang.org/genproto/googleapis/api/expr/conformance/v1alpha1"
2021
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
@@ -138,6 +139,10 @@ func processLoop() int {
138139
}
139140
}
140141

142+
if req.CelSource == "test_crash" {
143+
os.Exit(2)
144+
}
145+
141146
if err = c.serialize(writer, &resp); err != nil {
142147
fmt.Fprintf(os.Stderr, "error serializing parse resp %v\n", err)
143148
return 1
@@ -182,6 +187,17 @@ func processLoop() int {
182187
fmt.Fprintf(os.Stderr, "error serializing check resp %v\n", err)
183188
return 1
184189
}
190+
case "ping":
191+
req := emptypb.Empty{}
192+
if err := c.unmarshal(msg, &req); err != nil {
193+
fmt.Fprintf(os.Stderr, "bad ping req: %v\n", err)
194+
return 1
195+
}
196+
resp := emptypb.Empty{}
197+
if err = c.serialize(writer, &resp); err != nil {
198+
fmt.Fprintf(os.Stderr, "error serializing ping resp %v\n", err)
199+
return 1
200+
}
185201
default:
186202
fmt.Fprintf(os.Stderr, "unsupported cmd: %s\n", cmd)
187203
return 1

0 commit comments

Comments
 (0)