mgrpc: initial implementation of server
With this change, we define a simple server and client framework to start generating code against. We define a simple handler system with back registration into the server definition. From here, we can start generating code against the handlers. Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
789a1bff64
commit
42ff40f1f1
51
client.go
Normal file
51
client.go
Normal file
@ -0,0 +1,51 @@
|
||||
package mgrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/containerd/typeurl"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
channel *channel
|
||||
}
|
||||
|
||||
func NewClient(conn net.Conn) *Client {
|
||||
return &Client{
|
||||
channel: newChannel(conn),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
request := Request{
|
||||
Service: service,
|
||||
Method: method,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
if err := c.channel.send(ctx, &request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response Response
|
||||
|
||||
if err := c.channel.recv(ctx, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
13
handlers.go
Normal file
13
handlers.go
Normal file
@ -0,0 +1,13 @@
|
||||
package mgrpc
|
||||
|
||||
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)
|
||||
}
|
131
server.go
Normal file
131
server.go
Normal file
@ -0,0 +1,131 @@
|
||||
package mgrpc
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{handlers: make(map[string]map[string]Handler)}
|
||||
}
|
||||
|
||||
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) Shutdown(ctx context.Context) error {
|
||||
// TODO(stevvooe): Wait on connection shutdown.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Serve(l net.Listener) error {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
log.L.WithError(err).Error("failed accept")
|
||||
}
|
||||
|
||||
go s.handleConn(conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const maxMessageSize = 1 << 20 // TODO(stevvooe): Cut these down, since they are pre-alloced.
|
||||
|
||||
func (s *Server) handleConn(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
var (
|
||||
ch = newChannel(conn)
|
||||
req Request
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
)
|
||||
|
||||
defer cancel()
|
||||
|
||||
// TODO(stevvooe): Recover here or in dispatch to handle panics in service
|
||||
// methods.
|
||||
|
||||
// every connection is just a simple in/out request loop. No complexity for
|
||||
// multiplexing streams or dealing with head of line blocking, as this
|
||||
// isn't necessary for shim control.
|
||||
for {
|
||||
if err := ch.recv(ctx, &req); err != nil {
|
||||
log.L.WithError(err).Error("failed receiving message on channel")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.dispatch(ctx, &req)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Error("failed to dispatch request")
|
||||
return
|
||||
}
|
||||
|
||||
if err := ch.send(ctx, resp); err != nil {
|
||||
log.L.WithError(err).Error("failed sending message on channel")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
71
server_test.go
Normal file
71
server_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package mgrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
// var serverMethods = map[string]Handler{
|
||||
// "Create": HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
|
||||
// },
|
||||
// }
|
||||
|
||||
type testPayload struct {
|
||||
Foo string `protobuf:"bytes,1,opt,name=foo,proto3"`
|
||||
}
|
||||
|
||||
func (r *testPayload) Reset() { *r = testPayload{} }
|
||||
func (r *testPayload) String() string { return fmt.Sprintf("%+#v", r) }
|
||||
func (r *testPayload) ProtoMessage() {}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*testPayload)(nil), "testpayload")
|
||||
proto.RegisterType((*Request)(nil), "Request")
|
||||
proto.RegisterType((*Response)(nil), "Response")
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
server := NewServer()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := server.Register("test-service", map[string]Handler{
|
||||
"Test": HandlerFunc(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
fmt.Println(req)
|
||||
|
||||
return &testPayload{Foo: "baz"}, nil
|
||||
}),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
go server.Serve(listener)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := NewClient(conn)
|
||||
|
||||
tp := &testPayload{
|
||||
Foo: "bar",
|
||||
}
|
||||
resp, err := client.Call(ctx, "test-service", "Test", tp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(resp)
|
||||
}
|
27
types.go
Normal file
27
types.go
Normal file
@ -0,0 +1,27 @@
|
||||
package mgrpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/protobuf/google/rpc"
|
||||
"github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
func (r *Request) Reset() { *r = Request{} }
|
||||
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"`
|
||||
}
|
||||
|
||||
func (r *Response) Reset() { *r = Response{} }
|
||||
func (r *Response) String() string { return fmt.Sprintf("%+#v", r) }
|
||||
func (r *Response) ProtoMessage() {}
|
Loading…
Reference in New Issue
Block a user