diff --git a/client.go b/client.go new file mode 100644 index 0000000..29c746a --- /dev/null +++ b/client.go @@ -0,0 +1,119 @@ +package graphpipe + +import ( + fb "github.com/google/flatbuffers/go" + graphpipefb "github.com/oracle/graphpipe-go/graphpipefb" +) + +// BuildInferRequest constructs an InferRequest flatbuffer from NativeTensor +func BuildInferRequest(config string, inputTensors []*NativeTensor, inputs, outputs []string) (*fb.Builder, fb.UOffsetT) { + b := fb.NewBuilder(1024) + inStrs := make([]fb.UOffsetT, len(inputs)) + outStrs := make([]fb.UOffsetT, len(outputs)) + + for i := range inputs { + inStr := b.CreateString(inputs[i]) + inStrs[i] = inStr + } + + for i := range outputs { + outStr := b.CreateString(outputs[i]) + outStrs[i] = outStr + } + graphpipefb.InferRequestStartInputNamesVector(b, len(inputs)) + + for _, offset := range inStrs { + b.PrependUOffsetT(offset) + } + inputNamesOffset := b.EndVector(len(inputs)) + graphpipefb.InferRequestStartOutputNamesVector(b, len(outputs)) + for _, offset := range outStrs { + b.PrependUOffsetT(offset) + } + outputNamesOffset := b.EndVector(len(outputs)) + + inputOffsets := make([]fb.UOffsetT, len(inputTensors)) + for i := 0; i < len(inputTensors); i++ { + tp := inputTensors[i] + inputOffsets[i] = tp.Build(b) + } + + graphpipefb.InferRequestStartInputTensorsVector(b, 1) + for _, offset := range inputOffsets { + b.PrependUOffsetT(offset) + } + inputTensorsOffset := b.EndVector(1) + + configString := b.CreateString(config) + graphpipefb.InferRequestStart(b) + graphpipefb.InferRequestAddInputNames(b, inputNamesOffset) + graphpipefb.InferRequestAddOutputNames(b, outputNamesOffset) + graphpipefb.InferRequestAddInputTensors(b, inputTensorsOffset) + graphpipefb.InferRequestAddConfig(b, configString) + inferRequestOffset := graphpipefb.InferRequestEnd(b) + return b, inferRequestOffset +} + +// ParseInferResponse constructs a NativeTensor from flatbuffer +func ParseInferResponse(inferResponse *graphpipefb.InferResponse) []*NativeTensor { + tensors := []*NativeTensor{} + + for i := 0; i < inferResponse.OutputTensorsLength(); i++ { + + t := graphpipefb.Tensor{} + inferResponse.OutputTensors(&t, i) + shape := []int64{} + for j := 0; j < t.ShapeLength(); j++ { + shape = append(shape, t.Shape(j)) + } + nt := &NativeTensor{} + nt.InitWithData(t.DataBytes(), shape, t.Type()) + tensors = append(tensors, nt) + + } + return tensors +} + +// BuildMetadataRequest constructs flatbuffer from NativeMetadataRequest +func BuildMetadataRequest() (*fb.Builder, fb.UOffsetT) { + b := fb.NewBuilder(1024) + graphpipefb.MetadataRequestStart(b) + metaReq := graphpipefb.MetadataRequestEnd(b) + + graphpipefb.RequestStart(b) + graphpipefb.RequestAddReq(b, metaReq) + graphpipefb.RequestAddReqType(b, graphpipefb.ReqMetadataRequest) + req := graphpipefb.RequestEnd(b) + return b, req +} + +func parseIO(io *graphpipefb.IOMetadata) NativeIOMetadata { + nio := NativeIOMetadata{} + nio.Name = string(io.Name()) + nio.Description = string(io.Description()) + nio.Type = io.Type() + for i := 0; i < io.ShapeLength(); i++ { + nio.Shape = append(nio.Shape, io.Shape(i)) + } + return nio +} + +// ParseMetadataResponse constructs a NativeMetadataRequest from flatbuffer +func ParseMetadataResponse(metadataResponse *graphpipefb.MetadataResponse) *NativeMetadataResponse { + nm := &NativeMetadataResponse{} + nm.Version = string(metadataResponse.Version()) + nm.Server = string(metadataResponse.Server()) + nm.Description = string(metadataResponse.Description()) + + for i := 0; i < metadataResponse.InputsLength(); i++ { + io := &graphpipefb.IOMetadata{} + metadataResponse.Inputs(io, i) + nm.Inputs = append(nm.Inputs, parseIO(io)) + } + for i := 0; i < metadataResponse.OutputsLength(); i++ { + io := &graphpipefb.IOMetadata{} + metadataResponse.Outputs(io, i) + nm.Outputs = append(nm.Outputs, parseIO(io)) + } + return nm +} diff --git a/cmd/graphpipe-echo/main.go b/cmd/graphpipe-echo/main.go index 2219d58..9608898 100644 --- a/cmd/graphpipe-echo/main.go +++ b/cmd/graphpipe-echo/main.go @@ -1,20 +1,46 @@ package main import ( + "os" + "github.com/Sirupsen/logrus" graphpipe "github.com/oracle/graphpipe-go" + "github.com/spf13/cobra" ) -func main() { - logrus.SetLevel(logrus.InfoLevel) +type options struct { + listen string +} + +func runEchoServer(listen string) { useCache := false // toggle caching on/off inShapes := [][]int64(nil) // Optionally set input shapes outShapes := [][]int64(nil) // Optionally set output shapes - if err := graphpipe.Serve("0.0.0.0:9000", useCache, apply, inShapes, outShapes); err != nil { + if err := graphpipe.Serve(listen, useCache, apply, inShapes, outShapes); err != nil { logrus.Errorf("Failed to serve: %v", err) } } +func main() { + var opts options + var cmdExitCode int + logrus.SetLevel(logrus.InfoLevel) + + cmd := cobra.Command{ + Use: "graphpipe-echo", + Short: "graphpipe-echo - echoing ml requests", + Run: func(cmd *cobra.Command, args []string) { + runEchoServer(opts.listen) + }, + } + + f := cmd.Flags() + f.StringVarP(&opts.listen, "listen", "l", "127.0.0.1:9000", "listen string") + + cmd.Execute() + os.Exit(cmdExitCode) +} + func apply(requestContext *graphpipe.RequestContext, ignore string, in interface{}) interface{} { return in // using the graphpipe.Serve interface, graphpipe automatically converts go native types to tensors. } diff --git a/examples/grpc_infer_client/main.go b/examples/grpc_infer_client/main.go new file mode 100644 index 0000000..092b3d5 --- /dev/null +++ b/examples/grpc_infer_client/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + graphpipe "github.com/oracle/graphpipe-go" +) + +var addr = "grpc+http://127.0.0.1:9000" + +/// Example of a client that talks to graphpipe-echo +func main() { + v := []int64{2, 2} + output, err := graphpipe.Remote(addr, v) + if err != nil { + fmt.Println(err) + } else { + echoData := output.([]int64) + fmt.Println(echoData) + + } +} diff --git a/examples/grpc_metadata_client/main.go b/examples/grpc_metadata_client/main.go new file mode 100644 index 0000000..dfdcb1d --- /dev/null +++ b/examples/grpc_metadata_client/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + graphpipe "github.com/oracle/graphpipe-go" +) + +var addr = "grpc+http://127.0.0.1:9000" + +/// Example of a client that requests metadata from a graphpipe server +func main() { + meta, err := graphpipe.Metadata(addr) + + if err != nil { + fmt.Println(err) + } else { + fmt.Println(meta) + } +} diff --git a/examples/http_infer_client/main.go b/examples/http_infer_client/main.go new file mode 100644 index 0000000..5cf2086 --- /dev/null +++ b/examples/http_infer_client/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + graphpipe "github.com/oracle/graphpipe-go" +) + +var addr = "http://127.0.0.1:9000" + +/// Example of a client that talks to graphpipe-echo +func main() { + v := []int64{2, 2} + output, err := graphpipe.Remote(addr, v) + + if err != nil { + fmt.Printf("Failed to call remote: %v", err) + } else { + echoData := output.([]int64) + fmt.Println(echoData) + } +} diff --git a/examples/http_metadata_client/main.go b/examples/http_metadata_client/main.go new file mode 100644 index 0000000..6f59483 --- /dev/null +++ b/examples/http_metadata_client/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + graphpipe "github.com/oracle/graphpipe-go" +) + +var addr = "http://127.0.0.1:9000" + +/// Example of a client that requests metadata from a graphpipe server +func main() { + response, err := graphpipe.Metadata(addr) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(response) + } +} diff --git a/graphpipefb/GraphpipeService_grpc.go b/graphpipefb/GraphpipeService_grpc.go new file mode 100644 index 0000000..b2d5a92 --- /dev/null +++ b/graphpipefb/GraphpipeService_grpc.go @@ -0,0 +1,106 @@ +//Generated by gRPC Go plugin +//If you make any local changes, they will be lost +//source: graphpipe + +package graphpipe + +import "github.com/google/flatbuffers/go" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Client API for GraphpipeService service +type GraphpipeServiceClient interface{ + Infer(ctx context.Context, in *flatbuffers.Builder, + opts... grpc.CallOption) (* InferResponse, error) + Metadata(ctx context.Context, in *flatbuffers.Builder, + opts... grpc.CallOption) (* MetadataResponse, error) +} + +type graphpipeServiceClient struct { + cc *grpc.ClientConn +} + +func NewGraphpipeServiceClient(cc *grpc.ClientConn) GraphpipeServiceClient { + return &graphpipeServiceClient{cc} +} + +func (c *graphpipeServiceClient) Infer(ctx context.Context, in *flatbuffers.Builder, + opts... grpc.CallOption) (* InferResponse, error) { + out := new(InferResponse) + err := grpc.Invoke(ctx, "/graphpipe.GraphpipeService/Infer", in, out, c.cc, opts...) + if err != nil { return nil, err } + return out, nil +} + +func (c *graphpipeServiceClient) Metadata(ctx context.Context, in *flatbuffers.Builder, + opts... grpc.CallOption) (* MetadataResponse, error) { + out := new(MetadataResponse) + err := grpc.Invoke(ctx, "/graphpipe.GraphpipeService/Metadata", in, out, c.cc, opts...) + if err != nil { return nil, err } + return out, nil +} + +// Server API for GraphpipeService service +type GraphpipeServiceServer interface { + Infer(context.Context, *InferRequest) (*flatbuffers.Builder, error) + Metadata(context.Context, *MetadataRequest) (*flatbuffers.Builder, error) +} + +func RegisterGraphpipeServiceServer(s *grpc.Server, srv GraphpipeServiceServer) { + s.RegisterService(&_GraphpipeService_serviceDesc, srv) +} + +func _GraphpipeService_Infer_Handler(srv interface{}, ctx context.Context, + dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InferRequest) + if err := dec(in); err != nil { return nil, err } + if interceptor == nil { return srv.(GraphpipeServiceServer).Infer(ctx, in) } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/graphpipe.GraphpipeService/Infer", + } + + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GraphpipeServiceServer).Infer(ctx, req.(* InferRequest)) + } + return interceptor(ctx, in, info, handler) +} + + +func _GraphpipeService_Metadata_Handler(srv interface{}, ctx context.Context, + dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MetadataRequest) + if err := dec(in); err != nil { return nil, err } + if interceptor == nil { return srv.(GraphpipeServiceServer).Metadata(ctx, in) } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/graphpipe.GraphpipeService/Metadata", + } + + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GraphpipeServiceServer).Metadata(ctx, req.(* MetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + + +var _GraphpipeService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "graphpipe.GraphpipeService", + HandlerType: (*GraphpipeServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Infer", + Handler: _GraphpipeService_Infer_Handler, + }, + { + MethodName: "Metadata", + Handler: _GraphpipeService_Metadata_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + }, +} + diff --git a/remote.go b/remote.go index ddff7c3..4062754 100644 --- a/remote.go +++ b/remote.go @@ -2,6 +2,7 @@ package graphpipe import ( "bytes" + "errors" "fmt" "io/ioutil" "net/http" @@ -9,6 +10,8 @@ import ( "github.com/Sirupsen/logrus" fb "github.com/google/flatbuffers/go" graphpipefb "github.com/oracle/graphpipe-go/graphpipefb" + "golang.org/x/net/context" + "google.golang.org/grpc" ) // Remote is the simple function for making a remote model request with a @@ -16,11 +19,118 @@ import ( // automatic type conversion on its input and output. It will use the server // defaults for input and output. func Remote(uri string, in interface{}) (interface{}, error) { - res, err := MultiRemote(http.DefaultClient, uri, "", []interface{}{in}, nil, nil) - if len(res) != 1 { - return nil, fmt.Errorf("%d outputs were returned - one was expected", len(res)) + scheme, host, err := parseURL(uri) + + if err != nil { + return nil, err + } + switch scheme { + case "http": + res, err := MultiRemote(http.DefaultClient, uri, "", []interface{}{in}, nil, nil) + if len(res) != 1 { + return nil, fmt.Errorf("%d outputs were returned - one was expected", len(res)) + } + return res[0], err + case "grpc+http": + conn, err := getDefaultGRPCClient(host) + if err != nil { + fmt.Printf("Failed to connect: %v", err) + return nil, err + } + defer conn.Close() + client := graphpipefb.NewGraphpipeServiceClient(conn) + res, err := MultiRemote(client, uri, "", []interface{}{in}, nil, nil) + if len(res) != 1 { + return nil, fmt.Errorf("%d outputs were returned - one was expected", len(res)) + } + return res[0], err + default: + msg := fmt.Sprintf("Unhandled scheme: %s", scheme) + logrus.Errorf(msg) + return nil, errors.New(msg) + } + +} + +func getDefaultGRPCClient(host string) (*grpc.ClientConn, error) { + conn, err := grpc.Dial(host, grpc.WithInsecure(), grpc.WithCodec(fb.FlatbuffersCodec{})) + if err != nil { + fmt.Printf("Failed to connect: %v", err) + return nil, err + } + return conn, nil +} + +func grpcMetadata(host string) (*NativeMetadataResponse, error) { + b, offset := BuildMetadataRequest() + b.Finish(offset) + conn, err := getDefaultGRPCClient(host) + if err != nil { + fmt.Printf("Failed to connect: %v", err) + return nil, err + } + defer conn.Close() + client := graphpipefb.NewGraphpipeServiceClient(conn) + out, err := client.Metadata(context.Background(), b) + response := ParseMetadataResponse(out) + return response, nil +} + +func httpMetadata(uri string) (*NativeMetadataResponse, error) { + client := http.DefaultClient + + b, offset := BuildMetadataRequest() + + graphpipefb.RequestStart(b) + graphpipefb.RequestAddReqType(b, graphpipefb.ReqMetadataRequest) + graphpipefb.RequestAddReq(b, offset) + requestOffset := graphpipefb.RequestEnd(b) + buf := Serialize(b, requestOffset) + + rq, err := http.NewRequest("POST", uri, bytes.NewReader(buf)) + if err != nil { + logrus.Errorf("Failed to create request: %v", err) + return nil, err + } + + // send the request + rs, err := client.Do(rq) + if err != nil { + logrus.Errorf("Failed to send request: %v", err) + return nil, err + } + defer rs.Body.Close() + + body, err := ioutil.ReadAll(rs.Body) + if err != nil { + logrus.Errorf("Failed to read body: %v", err) + return nil, err + } + if rs.StatusCode != 200 { + return nil, fmt.Errorf("Remote failed with %d: %s", rs.StatusCode, string(body)) + } + res := graphpipefb.GetRootAsMetadataResponse(body, 0) + + return ParseMetadataResponse(res), err +} + +// Metadata is used to fetch metadata from a remote server +func Metadata(uri string) (*NativeMetadataResponse, error) { + scheme, host, err := parseURL(uri) + if err != nil { + return nil, err + } + + switch scheme { + case "http": + return httpMetadata(uri) + case "grpc+http": + return grpcMetadata(host) + default: + msg := fmt.Sprintf("Unhandled scheme: %s", scheme) + logrus.Errorf(msg) + return nil, errors.New(msg) } - return res[0], err } // MultiRemote is the complicated function for making a remote model request. @@ -30,7 +140,7 @@ func Remote(uri string, in interface{}) (interface{}, error) { // that you Specify inputNames and outputNames so that you can control // input/output ordering. MultiRemote also performs type introspection for // inputs and outputs. -func MultiRemote(client *http.Client, uri string, config string, ins []interface{}, inputNames, outputNames []string) ([]interface{}, error) { +func MultiRemote(clientInterface interface{}, uri string, config string, ins []interface{}, inputNames, outputNames []string) ([]interface{}, error) { inputs := make([]*NativeTensor, len(ins)) for i := range ins { var err error @@ -43,11 +153,12 @@ func MultiRemote(client *http.Client, uri string, config string, ins []interface } } - outputs, err := MultiRemoteRaw(client, uri, config, inputs, inputNames, outputNames) + outputs, err := MultiRemoteRaw(clientInterface, uri, config, inputs, inputNames, outputNames) if err != nil { logrus.Errorf("Failed to MultiRemoteRaw: %v", err) return nil, err } + natives := make([]interface{}, len(outputs)) for i := range outputs { var err error @@ -64,98 +175,61 @@ func MultiRemote(client *http.Client, uri string, config string, ins []interface // request using NativeTensor objects. The raw call is provided // for requests that need optimal performance and do not need to // be converted into native go types. -func MultiRemoteRaw(client *http.Client, uri string, config string, inputs []*NativeTensor, inputNames, outputNames []string) ([]*NativeTensor, error) { - b := fb.NewBuilder(1024) - - inStrs := make([]fb.UOffsetT, len(inputNames)) - outStrs := make([]fb.UOffsetT, len(outputNames)) - - for i := range inStrs { - inStrs[i] = b.CreateString(inputNames[i]) - } - - for i := range outStrs { - outStrs[i] = b.CreateString(outputNames[i]) - } - - graphpipefb.InferRequestStartInputNamesVector(b, len(inStrs)) - for i := len(inStrs) - 1; i >= 0; i-- { - offset := inStrs[i] - b.PrependUOffsetT(offset) - } - - inputNamesOffset := b.EndVector(len(inStrs)) - - graphpipefb.InferRequestStartOutputNamesVector(b, len(outStrs)) - for i := len(outStrs) - 1; i >= 0; i-- { - offset := outStrs[i] - b.PrependUOffsetT(offset) - } - outputNamesOffset := b.EndVector(len(outStrs)) - - inputOffsets := make([]fb.UOffsetT, len(inputs)) - for i := 0; i < len(inputs); i++ { - inputOffsets[i] = inputs[i].Build(b) - } - - graphpipefb.InferRequestStartInputTensorsVector(b, len(inputs)) - for i := len(inputOffsets) - 1; i >= 0; i-- { - offset := inputOffsets[i] - b.PrependUOffsetT(offset) - } - inputTensors := b.EndVector(len(inputs)) - - configString := b.CreateString(config) - - graphpipefb.InferRequestStart(b) - graphpipefb.InferRequestAddInputNames(b, inputNamesOffset) - graphpipefb.InferRequestAddOutputNames(b, outputNamesOffset) - graphpipefb.InferRequestAddInputTensors(b, inputTensors) - graphpipefb.InferRequestAddConfig(b, configString) - inferRequestOffset := graphpipefb.InferRequestEnd(b) - graphpipefb.RequestStart(b) - graphpipefb.RequestAddReqType(b, graphpipefb.ReqInferRequest) - graphpipefb.RequestAddReq(b, inferRequestOffset) - requestOffset := graphpipefb.RequestEnd(b) - - buf := Serialize(b, requestOffset) - - rq, err := http.NewRequest("POST", uri, bytes.NewReader(buf)) +func MultiRemoteRaw(clientInterface interface{}, uri string, config string, inputs []*NativeTensor, inputNames, outputNames []string) ([]*NativeTensor, error) { + scheme, _, err := parseURL(uri) if err != nil { - logrus.Errorf("Failed to create request: %v", err) return nil, err } + b, inferRequestOffset := BuildInferRequest(config, inputs, inputNames, outputNames) + switch scheme { + case "http": + client := clientInterface.(*http.Client) + + graphpipefb.RequestStart(b) + graphpipefb.RequestAddReqType(b, graphpipefb.ReqInferRequest) + graphpipefb.RequestAddReq(b, inferRequestOffset) + requestOffset := graphpipefb.RequestEnd(b) + buf := Serialize(b, requestOffset) + + rq, err := http.NewRequest("POST", uri, bytes.NewReader(buf)) + if err != nil { + logrus.Errorf("Failed to create request: %v", err) + return nil, err + } - // send the request - rs, err := client.Do(rq) - if err != nil { - logrus.Errorf("Failed to send request: %v", err) - return nil, err - } - defer rs.Body.Close() + // send the request + rs, err := client.Do(rq) + if err != nil { + logrus.Errorf("Failed to send request: %v", err) + return nil, err + } + defer rs.Body.Close() - body, err := ioutil.ReadAll(rs.Body) - if err != nil { - logrus.Errorf("Failed to read body: %v", err) - return nil, err - } - if rs.StatusCode != 200 { - return nil, fmt.Errorf("Remote failed with %d: %s", rs.StatusCode, string(body)) - } + body, err := ioutil.ReadAll(rs.Body) + if err != nil { + logrus.Errorf("Failed to read body: %v", err) + return nil, err + } + if rs.StatusCode != 200 { + return nil, fmt.Errorf("Remote failed with %d: %s", rs.StatusCode, string(body)) + } - res := graphpipefb.GetRootAsInferResponse(body, 0) + inferResponse := graphpipefb.GetRootAsInferResponse(body, 0) - rval := make([]*NativeTensor, res.OutputTensorsLength()) + outputTensors := ParseInferResponse(inferResponse) - for i := 0; i < res.OutputTensorsLength(); i++ { - tensor := &graphpipefb.Tensor{} - if !res.OutputTensors(tensor, i) { - err := fmt.Errorf("Bad input tensor") + return outputTensors, nil + case "grpc+http": + client := clientInterface.(graphpipefb.GraphpipeServiceClient) + b.Finish(inferRequestOffset) + inferResponse, err := client.Infer(context.Background(), b) + if err != nil { return nil, err } - nt := TensorToNativeTensor(tensor) - rval[i] = nt + outputTensors := ParseInferResponse(inferResponse) + return outputTensors, nil + default: + msg := fmt.Sprintf("Unhandled scheme in MultiRemoteRaw %s", scheme) + return nil, errors.New(msg) } - - return rval, nil } diff --git a/server.go b/server.go index e4075c1..6b1a2a3 100644 --- a/server.go +++ b/server.go @@ -8,13 +8,18 @@ import ( "io/ioutil" "net" "net/http" + "net/url" + "strconv" "sync/atomic" "time" "github.com/Sirupsen/logrus" bolt "github.com/coreos/bbolt" fb "github.com/google/flatbuffers/go" + flatbuffers "github.com/google/flatbuffers/go" graphpipefb "github.com/oracle/graphpipe-go/graphpipefb" + "golang.org/x/net/context" + "google.golang.org/grpc" ) // Error is our wrapper around the error interface. @@ -117,7 +122,21 @@ type ServeRawOptions struct { GetHandler GetHandlerFunc } -// ServeRaw starts the model server. The listen address and port can be specified +func parseURL(listen string) (string, string, error) { + listenURL := listen + if _, err := strconv.Atoi(listenURL[:1]); err == nil { + listenURL = "http://" + listenURL + logrus.Infof("Converted listen param from %s to %s", listen, listenURL) + } + + u, err := url.Parse(listenURL) + if err != nil { + return "", "", err + } + return u.Scheme, u.Host, nil +} + +// ServeRaw starts the model server. The listen url can be specified // with the listen parameter. If cacheFile is not "" then caches will be stored // using it. context will be passed back to the handler func ServeRaw(opts *ServeRawOptions) error { @@ -131,6 +150,7 @@ func ServeRaw(opts *ServeRawOptions) error { isReady: 1, isAlive: 1, } + if opts.CacheFile != "" { c.db, err = bolt.Open(opts.CacheFile, 0600, &bolt.Options{Timeout: 1 * time.Second}) if err != nil { @@ -139,15 +159,50 @@ func ServeRaw(opts *ServeRawOptions) error { } defer c.db.Close() } - setupLifecycleRoutes(c) - http.Handle("/", appHandler{c, Handler}) - logrus.Infof("Listening on '%s'", opts.Listen) - err = ListenAndServe(opts.Listen, nil) + + scheme, ipPort, err := parseURL(opts.Listen) + if err != nil { + return err + } + + host, _, err := net.SplitHostPort(ipPort) if err != nil { - logrus.Errorf("Error trying to ListenAndServe: %v", err) + logrus.Errorf("Could not find port in %s", ipPort) return err } + if net.ParseIP(host) == nil { + logrus.Errorf("Can only bind ips for serving. Found ip %s", host) + } + + logrus.Infof("Attempting to serve with protocol %s on %s", scheme, ipPort) + switch scheme { + case "http": + setupLifecycleRoutes(c) + http.Handle("/", appHandler{c, Handler}) + logrus.Infof("Listening with %s on '%s'", scheme, ipPort) + err = ListenAndServe(ipPort, nil) + if err != nil { + logrus.Errorf("Error trying to ListenAndServe: %v", err) + return err + } + case "grpc+http": + listen, err := net.Listen("tcp", ipPort) + if err != nil { + logrus.Errorf("Error trying to Listen for GRPC: %v", err) + } + + logrus.Infof("Listening with %s on '%s'", scheme, ipPort) + ser := grpc.NewServer(grpc.CustomCodec(flatbuffers.FlatbuffersCodec{})) + graphpipefb.RegisterGraphpipeServiceServer(ser, c) + if err := ser.Serve(listen); err != nil { + logrus.Fatalf("Failed to serve: %v", err) + return err + } + default: + logrus.Errorf("Couldn't serve protocol %s", scheme) + } + return nil } @@ -212,6 +267,86 @@ func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { logrus.Infof("Request for %s took %s", r.URL.Path, duration) } +func safeGetResults(c *appContext, requestContext *RequestContext, inferRequest *graphpipefb.InferRequest) (outputs []*NativeTensor, err error) { + defer func() { + if r := recover(); r != nil { + msg := fmt.Sprintf("panic recovered: %s", r) + logrus.Errorf(msg) + err = errors.New(msg) + } + }() + + if c.db == nil { + outputs, err = getResults(c, requestContext, inferRequest) + } else { + outputs, err = getResultsCached(c, requestContext, inferRequest) + } + return outputs, err +} + +func (c *appContext) _Infer(inferRequest *graphpipefb.InferRequest, requestContext *RequestContext) (*flatbuffers.Builder, error) { + var outputs []*NativeTensor + var err error + outputs, err = safeGetResults(c, requestContext, inferRequest) + + if requestContext.CleanupFunc != nil { + defer requestContext.CleanupFunc() + } + + b := requestContext.builder + + var errOffset flatbuffers.UOffsetT + if err != nil { + errStr := b.CreateString(err.Error()) + graphpipefb.ErrorStart(b) + graphpipefb.ErrorAddCode(b, 1) + graphpipefb.ErrorAddMessage(b, errStr) + offset := graphpipefb.ErrorEnd(b) + graphpipefb.InferResponseStartErrorsVector(b, 1) + b.PrependUOffsetT(offset) + errOffset = b.EndVector(1) + } + + outputOffsets := make([]fb.UOffsetT, len(outputs)) + for i := 0; i < len(outputs); i++ { + outputOffsets[i] = outputs[i].Build(b) + } + + graphpipefb.InferResponseStartOutputTensorsVector(b, len(outputOffsets)) + for i := len(outputOffsets) - 1; i >= 0; i-- { + offset := outputOffsets[i] + b.PrependUOffsetT(offset) + } + + tensors := b.EndVector(len(outputOffsets)) + graphpipefb.InferResponseStart(b) + graphpipefb.InferResponseAddOutputTensors(b, tensors) + + if errOffset != 0 { + graphpipefb.InferResponseAddErrors(b, errOffset) + } + + offset := graphpipefb.InferResponseEnd(b) + b.Finish(offset) + return b, nil +} + +func (c *appContext) Infer(context context.Context, inferRequest *graphpipefb.InferRequest) (*flatbuffers.Builder, error) { + requestContext := &RequestContext{ + builder: fb.NewBuilder(1024), + } + + b, err := c._Infer(inferRequest, requestContext) + return b, err +} + +func (c *appContext) Metadata(context context.Context, in *graphpipefb.MetadataRequest) (*flatbuffers.Builder, error) { + b := fb.NewBuilder(1024) + offset := c.meta.Build(b) + b.Finish(offset) + return b, nil +} + // Handler handles our http requests. func Handler(c *appContext, w http.ResponseWriter, r *http.Request) error { body, err := ioutil.ReadAll(r.Body) @@ -229,11 +364,6 @@ func Handler(c *appContext, w http.ResponseWriter, r *http.Request) error { request := graphpipefb.GetRootAsRequest(body, 0) if request.ReqType() == graphpipefb.ReqInferRequest { - inferRequest := &graphpipefb.InferRequest{} - table := inferRequest.Table() - request.Req(&table) - inferRequest.Init(table.Bytes, table.Pos) - requestContext := &RequestContext{ builder: fb.NewBuilder(1024), } @@ -251,37 +381,16 @@ func Handler(c *appContext, w http.ResponseWriter, r *http.Request) error { } requestContext.SetDead() }() - - var outputs []*NativeTensor - if c.db == nil { - outputs, err = getResults(c, requestContext, inferRequest) - } else { - outputs, err = getResultsCached(c, requestContext, inferRequest) - } - if requestContext.CleanupFunc != nil { - defer requestContext.CleanupFunc() - } + inferRequest := &graphpipefb.InferRequest{} + table := inferRequest.Table() + request.Req(&table) + inferRequest.Init(table.Bytes, table.Pos) + b, err := c._Infer(inferRequest, requestContext) if err != nil { - return StatusError{400, err} - } - b := requestContext.builder - - outputOffsets := make([]fb.UOffsetT, len(outputs)) - for i := 0; i < len(outputs); i++ { - outputOffsets[i] = outputs[i].Build(b) - } - - graphpipefb.InferResponseStartOutputTensorsVector(b, len(outputOffsets)) - for i := len(outputOffsets) - 1; i >= 0; i-- { - offset := outputOffsets[i] - b.PrependUOffsetT(offset) + return err } - tensors := b.EndVector(len(outputOffsets)) - graphpipefb.InferResponseStart(b) - graphpipefb.InferResponseAddOutputTensors(b, tensors) - inferResponseOffset := graphpipefb.InferResponseEnd(b) - tmp := Serialize(b, inferResponseOffset) + tmp := b.FinishedBytes() io.Copy(w, bytes.NewReader(tmp)) return nil @@ -292,7 +401,6 @@ func Handler(c *appContext, w http.ResponseWriter, r *http.Request) error { tmp := Serialize(b, offset) io.Copy(w, bytes.NewReader(tmp)) return nil - // return errors.New("Unhandled request type") } func isReadyHandler(c *appContext, w http.ResponseWriter, r *http.Request) error { diff --git a/vendor/vendor.json b/vendor/vendor.json index 128fce3..d2eb7b6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1,42 +1,352 @@ { "comment": "", - "ignore": "test", + "ignore": "test tools", "package": [ { - "checksumSHA1": "jc/kgOJNacek94rHKuFgIsjWwZw=", + "checksumSHA1": "uqjFeI7DxzsQew9ma1Cgz7WB5ns=", "path": "github.com/Sirupsen/logrus", - "revision": "ea8897e79973357ba785ac2533559a6297e83c44", - "revisionTime": "2018-05-23T07:42:43Z" + "revision": "3791101e143bf0f32515ac23e831475684f61229", + "revisionTime": "2018-09-04T20:21:35Z" }, { - "checksumSHA1": "qFaKrhSla38BRAyaGz2UaZvH/Dk=", + "checksumSHA1": "gMvwEfkm16Uwp3f1q2sIfC0bA8c=", "path": "github.com/coreos/bbolt", - "revision": "af9db2027c98c61ecd8e17caa5bd265792b9b9a2", - "revisionTime": "2018-03-18T00:15:26Z" + "revision": "27f3df899770c498f95099da93a2fcb763767961", + "revisionTime": "2018-08-28T18:43:12Z" }, { - "checksumSHA1": "rpUDWHC5vDOMn9ttHQjAtV/eLaM=", + "checksumSHA1": "CbzNfOvaT1G3G9WILmsyjYfYTO0=", + "path": "github.com/golang/protobuf/proto", + "revision": "b27b920f9e71b439b873b17bf99f56467623814a", + "revisionTime": "2018-08-21T05:17:52Z" + }, + { + "checksumSHA1": "tkJPssYejSjuAwE2tdEnoEIj93Q=", + "path": "github.com/golang/protobuf/ptypes", + "revision": "b27b920f9e71b439b873b17bf99f56467623814a", + "revisionTime": "2018-08-21T05:17:52Z" + }, + { + "checksumSHA1": "G0aiY+KmzFsQLTNzRAGRhJNSj7A=", + "path": "github.com/golang/protobuf/ptypes/any", + "revision": "b27b920f9e71b439b873b17bf99f56467623814a", + "revisionTime": "2018-08-21T05:17:52Z" + }, + { + "checksumSHA1": "kjVDCbK5/WiHqP1g4GMUxm75jos=", + "path": "github.com/golang/protobuf/ptypes/duration", + "revision": "b27b920f9e71b439b873b17bf99f56467623814a", + "revisionTime": "2018-08-21T05:17:52Z" + }, + { + "checksumSHA1": "FdeygjOuyR2p5v9b0kNOtzfpjS4=", + "path": "github.com/golang/protobuf/ptypes/timestamp", + "revision": "b27b920f9e71b439b873b17bf99f56467623814a", + "revisionTime": "2018-08-21T05:17:52Z" + }, + { + "checksumSHA1": "qvgCsBGQOA7vLlOWvgkbCc6Mtqs=", "path": "github.com/google/flatbuffers/go", - "revision": "c7a797b9669b3c9cd42b9e9989b0c19428412ec2", - "revisionTime": "2018-06-04T19:02:08Z" + "revision": "615885e889c92306f16b963fc4f88d1a447debf4", + "revisionTime": "2018-09-06T18:08:08Z" }, { "checksumSHA1": "BGm8lKZmvJbf/YOJLeL1rw2WVjA=", "path": "golang.org/x/crypto/ssh/terminal", - "revision": "df8d4716b3472e4a531c33cedbe537dae921a1a9", - "revisionTime": "2018-04-26T19:26:21Z" + "revision": "0709b304e793a5edb4a2c0145f281ecdc20838a4", + "revisionTime": "2018-08-31T22:38:59Z" + }, + { + "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", + "path": "golang.org/x/net/context", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "pCY4YtdNKVBYRbNvODjx8hj0hIs=", + "path": "golang.org/x/net/http/httpguts", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "3p4xISa2iLZULxYfVsIUlHJ+PUk=", + "path": "golang.org/x/net/http2", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "KZniwnfpWkaTPhUQDUTvgex/7y0=", + "path": "golang.org/x/net/http2/hpack", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "RcrB7tgYS/GMW4QrwVdMOTNqIU8=", + "path": "golang.org/x/net/idna", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "UxahDzW2v4mf/+aFxruuupaoIwo=", + "path": "golang.org/x/net/internal/timeseries", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" }, { - "checksumSHA1": "VUb+SqeoloAw1A2Jz9IzkDNs06c=", + "checksumSHA1": "6ckrK99wkirarIfFNX4+AHWBEHM=", + "path": "golang.org/x/net/trace", + "revision": "161cd47e91fd58ac17490ef4d742dc98bb4cf60e", + "revisionTime": "2018-09-06T07:10:30Z" + }, + { + "checksumSHA1": "GKC8IFNbSdF5HKJVx+Cm/3ASlH8=", "path": "golang.org/x/sys/unix", - "revision": "c11f84a56e43e20a78cee75a7c034031ecf57d1f", - "revisionTime": "2018-05-25T13:55:20Z" + "revision": "917fdcba135dcbaccd57425db91723541b4427c8", + "revisionTime": "2018-09-07T20:16:59Z" }, { - "checksumSHA1": "m5QCvoZ2KaoLTG9VWjYNonM08b8=", + "checksumSHA1": "DssjltU28TeBlMe16egT15lW0VA=", "path": "golang.org/x/sys/windows", - "revision": "c11f84a56e43e20a78cee75a7c034031ecf57d1f", - "revisionTime": "2018-05-25T13:55:20Z" + "revision": "917fdcba135dcbaccd57425db91723541b4427c8", + "revisionTime": "2018-09-07T20:16:59Z" + }, + { + "checksumSHA1": "CbpjEkkOeh0fdM/V8xKDdI0AA88=", + "path": "golang.org/x/text/secure/bidirule", + "revision": "4ae1256249243a4eb350a9a372e126557f2aa346", + "revisionTime": "2018-09-06T22:13:12Z" + }, + { + "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=", + "path": "golang.org/x/text/transform", + "revision": "4ae1256249243a4eb350a9a372e126557f2aa346", + "revisionTime": "2018-09-06T22:13:12Z" + }, + { + "checksumSHA1": "Qw4qdlZHCnBurAPPrSt+EKPIngM=", + "path": "golang.org/x/text/unicode/bidi", + "revision": "4ae1256249243a4eb350a9a372e126557f2aa346", + "revisionTime": "2018-09-06T22:13:12Z" + }, + { + "checksumSHA1": "XJr6+rzzxASewSbC/SCStyGlmuw=", + "path": "golang.org/x/text/unicode/norm", + "revision": "4ae1256249243a4eb350a9a372e126557f2aa346", + "revisionTime": "2018-09-06T22:13:12Z" + }, + { + "checksumSHA1": "oUD15OBRSXt0t4P0s6HMjH/+iQo=", + "path": "google.golang.org/genproto/googleapis/rpc/status", + "revision": "11092d34479b07829b72e10713b159248caf5dad", + "revisionTime": "2018-08-31T17:14:23Z" + }, + { + "checksumSHA1": "HV/AvwdtsJE9iQjBxmZ89+75O1Q=", + "path": "google.golang.org/grpc", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "B+kZFVP8zRiQMpoEb39Mp2oSmqg=", + "path": "google.golang.org/grpc/balancer", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "lw+L836hLeH8+//le+C+ycddCCU=", + "path": "google.golang.org/grpc/balancer/base", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "DJ1AtOk4Pu7bqtUMob95Hw8HPNw=", + "path": "google.golang.org/grpc/balancer/roundrobin", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "R3tuACGAPyK4lr+oSNt1saUzC0M=", + "path": "google.golang.org/grpc/codes", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "XH2WYcDNwVO47zYShREJjcYXm0Y=", + "path": "google.golang.org/grpc/connectivity", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "wA6y5rkH1v4bWBe5M1r/Hdtgma4=", + "path": "google.golang.org/grpc/credentials", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "cfLb+pzWB+Glwp82rgfcEST1mv8=", + "path": "google.golang.org/grpc/encoding", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "LKKkn7EYA+Do9Qwb2/SUKLFNxoo=", + "path": "google.golang.org/grpc/encoding/proto", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "ZPPSFisPDz2ANO4FBZIft+fRxyk=", + "path": "google.golang.org/grpc/grpclog", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "cSdzm5GhbalJbWUNrN8pRdW0uks=", + "path": "google.golang.org/grpc/internal", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "uDJA7QK2iGnEwbd9TPqkLaM+xuU=", + "path": "google.golang.org/grpc/internal/backoff", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "fvj+rPmX5++NHHTTwG9gHlTEGow=", + "path": "google.golang.org/grpc/internal/channelz", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "5dFUCEaPjKwza9kwKqgljp8ckU4=", + "path": "google.golang.org/grpc/internal/envconfig", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "70gndc/uHwyAl3D45zqp7vyHWlo=", + "path": "google.golang.org/grpc/internal/grpcrand", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "F/QkfMhNApNewEK84d9CM3fT6js=", + "path": "google.golang.org/grpc/internal/transport", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "hcuHgKp8W0wIzoCnNfKI8NUss5o=", + "path": "google.golang.org/grpc/keepalive", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "OjIAi5AzqlQ7kLtdAyjvdgMf6hc=", + "path": "google.golang.org/grpc/metadata", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "VvGBoawND0urmYDy11FT+U1IHtU=", + "path": "google.golang.org/grpc/naming", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "n5EgDdBqFMa2KQFhtl+FF/4gIFo=", + "path": "google.golang.org/grpc/peer", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "GEq6wwE1qWLmkaM02SjxBmmnHDo=", + "path": "google.golang.org/grpc/resolver", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "3aaLCpBkkCNQvVPHyvPi+G0vnQI=", + "path": "google.golang.org/grpc/resolver/dns", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "zs9M4xE8Lyg4wvuYvR00XoBxmuw=", + "path": "google.golang.org/grpc/resolver/passthrough", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "YclPgme2gT3S0hTkHVdE1zAxJdo=", + "path": "google.golang.org/grpc/stats", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "t/NhHuykWsxY0gEBd2WIv5RVBK8=", + "path": "google.golang.org/grpc/status", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" + }, + { + "checksumSHA1": "qvArRhlrww5WvRmbyMF2mUfbJew=", + "path": "google.golang.org/grpc/tap", + "revision": "32fb0ac620c32ba40a4626ddf94d90d12cce3455", + "revisionTime": "2018-07-31T16:43:25Z", + "version": "v1.14.0", + "versionExact": "v1.14.0" } ], "rootPath": "github.com/oracle/graphpipe-go"