ttrpc: remove use of typeurl
Rather than employ the typeurl package, we now generate code to correctly allocate the incoming types from the caller. As a side-effect of this activity, the services definitions have been split out into a separate type that handles the full resolution and dispatch of the method, incuding correctly mapping the RPC status. This work is a pre-cursor to larger protocol change that will allow us to handle multiple, concurrent requests. Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
2d76dba1df
commit
2a81659f49
41
client.go
41
client.go
@ -4,7 +4,8 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/containerd/typeurl"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@ -17,10 +18,17 @@ func NewClient(conn net.Conn) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Call(ctx context.Context, service, method string, req interface{}) (interface{}, error) {
|
||||
payload, err := typeurl.MarshalAny(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *Client) Call(ctx context.Context, service, method string, req, resp interface{}) error {
|
||||
var payload []byte
|
||||
switch v := req.(type) {
|
||||
case proto.Message:
|
||||
var err error
|
||||
payload, err = proto.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("ttrpc: unknown request type: %T", req)
|
||||
}
|
||||
|
||||
request := Request{
|
||||
@ -30,22 +38,21 @@ func (c *Client) Call(ctx context.Context, service, method string, req interface
|
||||
}
|
||||
|
||||
if err := c.channel.send(ctx, &request); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var response Response
|
||||
|
||||
if err := c.channel.recv(ctx, &response); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
switch v := resp.(type) {
|
||||
case proto.Message:
|
||||
if err := proto.Unmarshal(response.Payload, v); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("ttrpc: unknown response type: %T", resp)
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Reliance on the typeurl isn't great for bootstrapping
|
||||
// and ease of use. Let's consider a request header frame and body frame as
|
||||
// a better solution. This will allow the caller to set the exact type.
|
||||
rpayload, err := typeurl.UnmarshalAny(response.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rpayload, nil
|
||||
return nil
|
||||
}
|
||||
|
@ -278,14 +278,22 @@ type ExampleService interface {
|
||||
Method2(ctx context.Context, req *Method1Request) (*google_protobuf.Empty, error)
|
||||
}
|
||||
|
||||
func RegisterExampleService(srv *github_com_stevvooe_ttrpc.Server, svc ExampleService) error {
|
||||
return srv.Register("ttrpc.example.v1.Example", map[string]github_com_stevvooe_ttrpc.Handler{
|
||||
"Method1": github_com_stevvooe_ttrpc.HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return svc.Method1(ctx, req.(*Method1Request))
|
||||
}),
|
||||
"Method2": github_com_stevvooe_ttrpc.HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return svc.Method2(ctx, req.(*Method1Request))
|
||||
}),
|
||||
func RegisterExampleService(srv *github_com_stevvooe_ttrpc.Server, svc ExampleService) {
|
||||
srv.Register("ttrpc.example.v1.Example", map[string]github_com_stevvooe_ttrpc.Method{
|
||||
"Method1": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
|
||||
var req Method1Request
|
||||
if err := unmarshal(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.Method1(ctx, &req)
|
||||
},
|
||||
"Method2": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
|
||||
var req Method1Request
|
||||
if err := unmarshal(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.Method2(ctx, &req)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -300,19 +308,19 @@ func NewExampleClient(client *github_com_stevvooe_ttrpc.Client) ExampleService {
|
||||
}
|
||||
|
||||
func (c *exampleClient) Method1(ctx context.Context, req *Method1Request) (*Method1Response, error) {
|
||||
resp, err := c.client.Call(ctx, "ttrpc.example.v1.Example", "Method1", req)
|
||||
if err != nil {
|
||||
var resp Method1Response
|
||||
if err := c.client.Call(ctx, "ttrpc.example.v1.Example", "Method1", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.(*Method1Response), nil
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *exampleClient) Method2(ctx context.Context, req *Method1Request) (*google_protobuf.Empty, error) {
|
||||
resp, err := c.client.Call(ctx, "ttrpc.example.v1.Example", "Method2", req)
|
||||
if err != nil {
|
||||
var resp google_protobuf.Empty
|
||||
if err := c.client.Call(ctx, "ttrpc.example.v1.Example", "Method2", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.(*google_protobuf.Empty), nil
|
||||
return &resp, nil
|
||||
}
|
||||
func (m *Method1Request) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
|
13
handlers.go
13
handlers.go
@ -1,13 +0,0 @@
|
||||
package ttrpc
|
||||
|
||||
import "context"
|
||||
|
||||
type Handler interface {
|
||||
Handle(ctx context.Context, req interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
type HandlerFunc func(ctx context.Context, req interface{}) (interface{}, error)
|
||||
|
||||
func (fn HandlerFunc) Handle(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return fn(ctx, req)
|
||||
}
|
@ -61,16 +61,22 @@ func (p *ttrpcGenerator) genService(fullName string, service *descriptor.Service
|
||||
|
||||
p.P()
|
||||
// registration method
|
||||
p.P("func Register", serviceName, "(srv *", p.ttrpcPkg.Use(), ".Server, svc ", serviceName, ") error {")
|
||||
p.P("func Register", serviceName, "(srv *", p.ttrpcPkg.Use(), ".Server, svc ", serviceName, ") {")
|
||||
p.In()
|
||||
p.P(`return srv.Register("`, fullName, `", map[string]`, p.ttrpcPkg.Use(), ".Handler{")
|
||||
p.P(`srv.Register("`, fullName, `", map[string]`, p.ttrpcPkg.Use(), ".Method{")
|
||||
p.In()
|
||||
for _, method := range service.Method {
|
||||
p.P(`"`, method.GetName(), `": `, p.ttrpcPkg.Use(), `.HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {`)
|
||||
p.P(`"`, method.GetName(), `": `, `func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {`)
|
||||
p.In()
|
||||
p.P("return svc.", method.GetName(), "(ctx, req.(*", p.typeName(method.GetInputType()), "))")
|
||||
p.P("var req ", p.typeName(method.GetInputType()))
|
||||
p.P(`if err := unmarshal(&req); err != nil {`)
|
||||
p.In()
|
||||
p.P(`return nil, err`)
|
||||
p.Out()
|
||||
p.P("}),")
|
||||
p.P(`}`)
|
||||
p.P("return svc.", method.GetName(), "(ctx, &req)")
|
||||
p.Out()
|
||||
p.P("},")
|
||||
}
|
||||
p.Out()
|
||||
p.P("})")
|
||||
@ -103,13 +109,13 @@ func (p *ttrpcGenerator) genService(fullName string, service *descriptor.Service
|
||||
"req *", p.typeName(method.GetInputType()), ") ",
|
||||
"(*", p.typeName(method.GetOutputType()), ", error) {")
|
||||
p.In()
|
||||
p.P("resp, err := c.client.Call(ctx, ", `"`+fullName+`", `, `"`+method.GetName()+`"`, ", req)")
|
||||
p.P("if err != nil {")
|
||||
p.P("var resp ", p.typeName(method.GetOutputType()))
|
||||
p.P("if err := c.client.Call(ctx, ", `"`+fullName+`", `, `"`+method.GetName()+`"`, ", req, &resp); err != nil {")
|
||||
p.In()
|
||||
p.P("return nil, err")
|
||||
p.Out()
|
||||
p.P("}")
|
||||
p.P("return resp.(*", p.typeName(method.GetOutputType()), "), nil")
|
||||
p.P("return &resp, nil")
|
||||
p.Out()
|
||||
p.P("}")
|
||||
}
|
||||
|
74
server.go
74
server.go
@ -3,29 +3,22 @@ package ttrpc
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"path"
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/containerd/typeurl"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
handlers map[string]map[string]Handler
|
||||
services *serviceSet
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{handlers: make(map[string]map[string]Handler)}
|
||||
return &Server{
|
||||
services: newServiceSet(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Register(name string, methods map[string]Handler) error {
|
||||
if _, ok := s.handlers[name]; ok {
|
||||
return errors.Errorf("duplicate service %v registered", name)
|
||||
}
|
||||
|
||||
s.handlers[name] = methods
|
||||
return nil
|
||||
func (s *Server) Register(name string, methods map[string]Method) {
|
||||
s.services.register(name, methods)
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
@ -70,10 +63,11 @@ func (s *Server) handleConn(conn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.dispatch(ctx, &req)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Error("failed to dispatch request")
|
||||
return
|
||||
p, status := s.services.call(ctx, req.Service, req.Method, req.Payload)
|
||||
|
||||
resp := &Response{
|
||||
Status: status.Proto(),
|
||||
Payload: p,
|
||||
}
|
||||
|
||||
if err := ch.send(ctx, resp); err != nil {
|
||||
@ -82,49 +76,3 @@ func (s *Server) handleConn(conn net.Conn) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(ctx context.Context, req *Request) (*Response, error) {
|
||||
ctx = log.WithLogger(ctx, log.G(ctx).WithField("method", path.Join("/", req.Service, req.Method)))
|
||||
handler, err := s.resolve(req.Service, req.Method)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Error("failed to resolve handler")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := typeurl.UnmarshalAny(req.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := handler.Handle(ctx, payload)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Error("handler returned an error")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apayload, err := typeurl.MarshalAny(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rresp := &Response{
|
||||
// Status: *st,
|
||||
Payload: apayload,
|
||||
}
|
||||
|
||||
return rresp, nil
|
||||
}
|
||||
|
||||
func (s *Server) resolve(service, method string) (Handler, error) {
|
||||
srv, ok := s.handlers[service]
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(errdefs.ErrNotFound, "could not resolve service %v", service)
|
||||
}
|
||||
|
||||
handler, ok := srv[method]
|
||||
if !ok {
|
||||
return nil, errors.Wrapf(errdefs.ErrNotFound, "could not resolve method %v", method)
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
@ -31,12 +31,8 @@ func newTestingClient(client *Client) *testingClient {
|
||||
}
|
||||
|
||||
func (tc *testingClient) Test(ctx context.Context, req *testPayload) (*testPayload, error) {
|
||||
resp, err := tc.client.Call(ctx, serviceName, "Test", req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.(*testPayload), nil
|
||||
var tp testPayload
|
||||
return &tp, tc.client.Call(ctx, serviceName, "Test", req, &tp)
|
||||
}
|
||||
|
||||
type testPayload struct {
|
||||
@ -72,17 +68,19 @@ func TestServer(t *testing.T) {
|
||||
|
||||
// more mocking of what is generated code. Unlike grpc, we register with a
|
||||
// closure so that the descriptor is allocated only on registration.
|
||||
registerTestingService := func(srv *Server, svc testingService) error {
|
||||
return srv.Register(serviceName, map[string]Handler{
|
||||
"Test": HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return svc.Test(ctx, req.(*testPayload))
|
||||
}),
|
||||
registerTestingService := func(srv *Server, svc testingService) {
|
||||
srv.Register(serviceName, map[string]Method{
|
||||
"Test": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
|
||||
var req testPayload
|
||||
if err := unmarshal(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.Test(ctx, &req)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := registerTestingService(server, testImpl); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registerTestingService(server, testImpl)
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
|
136
services.go
Normal file
136
services.go
Normal file
@ -0,0 +1,136 @@
|
||||
package ttrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/containerd/containerd/log"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type Method func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error)
|
||||
|
||||
type ServiceDesc struct {
|
||||
Methods map[string]Method
|
||||
|
||||
// TODO(stevvooe): Add stream support.
|
||||
}
|
||||
|
||||
type serviceSet struct {
|
||||
services map[string]ServiceDesc
|
||||
}
|
||||
|
||||
func newServiceSet() *serviceSet {
|
||||
return &serviceSet{
|
||||
services: make(map[string]ServiceDesc),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceSet) register(name string, methods map[string]Method) {
|
||||
if _, ok := s.services[name]; ok {
|
||||
panic(errors.Errorf("duplicate service %v registered", name))
|
||||
}
|
||||
|
||||
s.services[name] = ServiceDesc{
|
||||
Methods: methods,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceSet) call(ctx context.Context, serviceName, methodName string, p []byte) ([]byte, *status.Status) {
|
||||
p, err := s.dispatch(ctx, serviceName, methodName, p)
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
st = status.New(convertCode(err), err.Error())
|
||||
}
|
||||
|
||||
return p, st
|
||||
}
|
||||
|
||||
func (s *serviceSet) dispatch(ctx context.Context, serviceName, methodName string, p []byte) ([]byte, error) {
|
||||
ctx = log.WithLogger(ctx, log.G(ctx).WithField("method", fullPath(serviceName, methodName)))
|
||||
method, err := s.resolve(serviceName, methodName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unmarshal := func(obj interface{}) error {
|
||||
switch v := obj.(type) {
|
||||
case proto.Message:
|
||||
if err := proto.Unmarshal(p, v); err != nil {
|
||||
return status.Errorf(codes.Internal, "ttrpc: error unmarshaling payload: %v", err.Error())
|
||||
}
|
||||
default:
|
||||
return status.Errorf(codes.Internal, "ttrpc: error unsupported request type: %T", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := method(ctx, unmarshal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := resp.(type) {
|
||||
case proto.Message:
|
||||
r, err := proto.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "ttrpc: error marshaling payload: %v", err.Error())
|
||||
}
|
||||
|
||||
return r, nil
|
||||
default:
|
||||
return nil, status.Errorf(codes.Internal, "ttrpc: error unsupported response type: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceSet) resolve(service, method string) (Method, error) {
|
||||
srv, ok := s.services[service]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "service %v", service)
|
||||
}
|
||||
|
||||
mthd, ok := srv.Methods[method]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "method %v", method)
|
||||
}
|
||||
|
||||
return mthd, nil
|
||||
}
|
||||
|
||||
// convertCode maps stdlib go errors into grpc space.
|
||||
//
|
||||
// This is ripped from the grpc-go code base.
|
||||
func convertCode(err error) codes.Code {
|
||||
switch err {
|
||||
case nil:
|
||||
return codes.OK
|
||||
case io.EOF:
|
||||
return codes.OutOfRange
|
||||
case io.ErrClosedPipe, io.ErrNoProgress, io.ErrShortBuffer, io.ErrShortWrite, io.ErrUnexpectedEOF:
|
||||
return codes.FailedPrecondition
|
||||
case os.ErrInvalid:
|
||||
return codes.InvalidArgument
|
||||
case context.Canceled:
|
||||
return codes.Canceled
|
||||
case context.DeadlineExceeded:
|
||||
return codes.DeadlineExceeded
|
||||
}
|
||||
switch {
|
||||
case os.IsExist(err):
|
||||
return codes.AlreadyExists
|
||||
case os.IsNotExist(err):
|
||||
return codes.NotFound
|
||||
case os.IsPermission(err):
|
||||
return codes.PermissionDenied
|
||||
}
|
||||
return codes.Unknown
|
||||
}
|
||||
|
||||
func fullPath(service, method string) string {
|
||||
return "/" + path.Join("/", service, method)
|
||||
}
|
13
types.go
13
types.go
@ -3,14 +3,13 @@ package ttrpc
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/protobuf/google/rpc"
|
||||
"github.com/gogo/protobuf/types"
|
||||
spb "google.golang.org/genproto/googleapis/rpc/status"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Service string `protobuf:"bytes,1,opt,name=service,proto3"`
|
||||
Method string `protobuf:"bytes,2,opt,name=method,proto3"`
|
||||
Payload *types.Any `protobuf:"bytes,3,opt,name=payload,proto3"`
|
||||
Service string `protobuf:"bytes,1,opt,name=service,proto3"`
|
||||
Method string `protobuf:"bytes,2,opt,name=method,proto3"`
|
||||
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3"`
|
||||
}
|
||||
|
||||
func (r *Request) Reset() { *r = Request{} }
|
||||
@ -18,8 +17,8 @@ func (r *Request) String() string { return fmt.Sprintf("%+#v", r) }
|
||||
func (r *Request) ProtoMessage() {}
|
||||
|
||||
type Response struct {
|
||||
Status *rpc.Status `protobuf:"bytes,1,opt,name=status,proto3"`
|
||||
Payload *types.Any `protobuf:"bytes,2,opt,name=payload,proto3"`
|
||||
Status *spb.Status `protobuf:"bytes,1,opt,name=status,proto3"`
|
||||
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3"`
|
||||
}
|
||||
|
||||
func (r *Response) Reset() { *r = Response{} }
|
||||
|
Loading…
Reference in New Issue
Block a user