micro-client-grpc/grpc.go

762 lines
17 KiB
Go
Raw Permalink Normal View History

2019-06-03 20:44:43 +03:00
// Package grpc provides a gRPC client
package grpc
import (
"context"
"crypto/tls"
"fmt"
"net"
"reflect"
"strings"
"sync"
"sync/atomic"
2019-06-03 20:44:43 +03:00
"time"
raw "github.com/unistack-org/micro-codec-bytes"
"github.com/unistack-org/micro/v3/broker"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/errors"
"github.com/unistack-org/micro/v3/metadata"
2019-06-03 20:44:43 +03:00
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
gmetadata "google.golang.org/grpc/metadata"
)
type grpcClient struct {
opts client.Options
codecs map[string]encoding.Codec
pool *pool
once atomic.Value
sync.RWMutex
2019-06-03 20:44:43 +03:00
}
func init() {
2019-06-18 20:51:52 +03:00
encoding.RegisterCodec(wrapCodec{jsonCodec{}})
2019-06-27 15:08:06 +03:00
encoding.RegisterCodec(wrapCodec{protoCodec{}})
2019-06-18 20:51:52 +03:00
encoding.RegisterCodec(wrapCodec{bytesCodec{}})
2019-06-03 20:44:43 +03:00
}
// secure returns the dial option for whether its a secure or insecure connection
func (g *grpcClient) secure(addr string) grpc.DialOption {
// first we check if theres'a tls config
2019-06-03 20:44:43 +03:00
if g.opts.Context != nil {
if v := g.opts.Context.Value(tlsAuth{}); v != nil {
tls := v.(*tls.Config)
creds := credentials.NewTLS(tls)
// return tls config if it exists
2019-06-03 20:44:43 +03:00
return grpc.WithTransportCredentials(creds)
}
}
// default config
tlsConfig := &tls.Config{}
defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
// check if the address is prepended with https
if strings.HasPrefix(addr, "https://") {
return defaultCreds
}
// if no port is specified or port is 443 default to tls
_, port, err := net.SplitHostPort(addr)
// assuming with no port its going to be secured
if port == "443" {
return defaultCreds
} else if err != nil && strings.Contains(err.Error(), "missing port in address") {
return defaultCreds
}
// other fallback to insecure
2019-06-03 20:44:43 +03:00
return grpc.WithInsecure()
}
func (g *grpcClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
var header map[string]string
header = make(map[string]string)
2019-06-03 20:44:43 +03:00
if md, ok := metadata.FromContext(ctx); ok {
header = make(map[string]string, len(md))
2019-06-03 20:44:43 +03:00
for k, v := range md {
header[strings.ToLower(k)] = v
2019-06-03 20:44:43 +03:00
}
} else {
header = make(map[string]string)
2019-06-03 20:44:43 +03:00
}
// set timeout in nanoseconds
header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout)
// set the content type for the request
header["x-content-type"] = req.ContentType()
2019-06-03 20:44:43 +03:00
md := gmetadata.New(header)
ctx = gmetadata.NewOutgoingContext(ctx, md)
cf, err := g.newGRPCCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
maxRecvMsgSize := g.maxRecvMsgSizeValue()
maxSendMsgSize := g.maxSendMsgSizeValue()
var grr error
var dialCtx context.Context
var cancel context.CancelFunc
if opts.DialTimeout >= 0 {
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
} else {
dialCtx, cancel = context.WithCancel(ctx)
}
defer cancel()
2019-10-09 09:45:51 +03:00
grpcDialOptions := []grpc.DialOption{
g.secure(addr),
2019-06-03 20:44:43 +03:00
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(maxRecvMsgSize),
grpc.MaxCallSendMsgSize(maxSendMsgSize),
2019-10-09 09:45:51 +03:00
),
}
if opts := g.getGrpcDialOptions(); opts != nil {
grpcDialOptions = append(grpcDialOptions, opts...)
}
cc, err := g.pool.getConn(dialCtx, addr, grpcDialOptions...)
2019-06-03 20:44:43 +03:00
if err != nil {
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
}
defer func() {
// defer execution of release
g.pool.release(addr, cc, grr)
2019-06-03 20:44:43 +03:00
}()
ch := make(chan error, 1)
go func() {
grpcCallOptions := []grpc.CallOption{
grpc.ForceCodec(cf),
grpc.CallContentSubtype(cf.Name())}
2019-10-09 09:45:51 +03:00
if opts := g.getGrpcCallOptions(); opts != nil {
grpcCallOptions = append(grpcCallOptions, opts...)
}
err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...)
2019-06-03 20:44:43 +03:00
ch <- microError(err)
}()
select {
case err := <-ch:
grr = err
case <-ctx.Done():
grr = errors.Timeout("go.micro.client", "%v", ctx.Err())
2019-06-03 20:44:43 +03:00
}
return grr
}
func (g *grpcClient) stream(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
var header map[string]string
2019-06-03 20:44:43 +03:00
if md, ok := metadata.FromContext(ctx); ok {
header = make(map[string]string, len(md))
2019-06-03 20:44:43 +03:00
for k, v := range md {
header[k] = v
}
} else {
header = make(map[string]string)
2019-06-03 20:44:43 +03:00
}
// set timeout in nanoseconds
if opts.StreamTimeout > time.Duration(0) {
header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
}
2019-06-03 20:44:43 +03:00
// set the content type for the request
header["x-content-type"] = req.ContentType()
md := gmetadata.New(header)
ctx = gmetadata.NewOutgoingContext(ctx, md)
cf, err := g.newGRPCCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
2019-06-03 20:44:43 +03:00
}
var dialCtx context.Context
var cancel context.CancelFunc
if opts.DialTimeout >= 0 {
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
} else {
dialCtx, cancel = context.WithCancel(ctx)
}
defer cancel()
2019-06-17 22:05:58 +03:00
wc := wrapCodec{cf}
2019-10-09 09:45:51 +03:00
grpcDialOptions := []grpc.DialOption{
g.secure(addr),
2019-10-09 09:45:51 +03:00
}
if opts := g.getGrpcDialOptions(); opts != nil {
grpcDialOptions = append(grpcDialOptions, opts...)
}
cc, err := g.pool.getConn(dialCtx, addr, grpcDialOptions...)
2019-06-03 20:44:43 +03:00
if err != nil {
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
2019-06-03 20:44:43 +03:00
}
desc := &grpc.StreamDesc{
StreamName: req.Service() + req.Endpoint(),
ClientStreams: true,
ServerStreams: true,
}
grpcCallOptions := []grpc.CallOption{
grpc.ForceCodec(wc),
grpc.CallContentSubtype(cf.Name()),
}
2019-10-09 09:45:51 +03:00
if opts := g.getGrpcCallOptions(); opts != nil {
grpcCallOptions = append(grpcCallOptions, opts...)
}
// create a new cancelling context
newCtx, cancel := context.WithCancel(ctx)
2020-01-20 01:53:56 +03:00
st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...)
2019-06-03 20:44:43 +03:00
if err != nil {
// we need to cleanup as we dialled and created a context
// cancel the context
cancel()
// release the connection
g.pool.release(addr, cc, err)
// now return the error
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err))
2019-06-03 20:44:43 +03:00
}
2019-06-18 20:51:52 +03:00
codec := &grpcCodec{
s: st,
c: wc,
}
2019-06-17 22:05:58 +03:00
// set request codec
if r, ok := req.(*grpcRequest); ok {
2019-06-18 20:51:52 +03:00
r.codec = codec
2019-06-17 22:05:58 +03:00
}
// setup the stream response
stream := &grpcStream{
ClientStream: st,
context: ctx,
request: req,
response: &response{
conn: cc,
stream: st,
codec: cf,
gcodec: codec,
},
conn: cc,
close: func(err error) {
// cancel the context if an error occured
if err != nil {
cancel()
}
// defer execution of release
g.pool.release(addr, cc, err)
},
}
// set the stream as the response
val := reflect.ValueOf(rsp).Elem()
val.Set(reflect.ValueOf(stream).Elem())
return nil
2019-06-03 20:44:43 +03:00
}
func (g *grpcClient) poolMaxStreams() int {
if g.opts.Context == nil {
return DefaultPoolMaxStreams
}
v := g.opts.Context.Value(poolMaxStreams{})
if v == nil {
return DefaultPoolMaxStreams
}
return v.(int)
}
func (g *grpcClient) poolMaxIdle() int {
if g.opts.Context == nil {
return DefaultPoolMaxIdle
}
v := g.opts.Context.Value(poolMaxIdle{})
if v == nil {
return DefaultPoolMaxIdle
}
return v.(int)
}
2019-06-03 20:44:43 +03:00
func (g *grpcClient) maxRecvMsgSizeValue() int {
if g.opts.Context == nil {
return DefaultMaxRecvMsgSize
}
v := g.opts.Context.Value(maxRecvMsgSizeKey{})
if v == nil {
return DefaultMaxRecvMsgSize
}
return v.(int)
}
func (g *grpcClient) maxSendMsgSizeValue() int {
if g.opts.Context == nil {
return DefaultMaxSendMsgSize
}
v := g.opts.Context.Value(maxSendMsgSizeKey{})
if v == nil {
return DefaultMaxSendMsgSize
}
return v.(int)
}
2019-06-11 11:52:35 +03:00
func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) {
g.RLock()
defer g.RUnlock()
if c, ok := g.codecs[contentType]; ok {
2019-06-03 20:44:43 +03:00
return wrapCodec{c}, nil
}
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
}
func (g *grpcClient) Init(opts ...client.Option) error {
size := g.opts.PoolSize
ttl := g.opts.PoolTTL
for _, o := range opts {
o(&g.opts)
}
// update pool configuration if the options changed
if size != g.opts.PoolSize || ttl != g.opts.PoolTTL {
g.pool.Lock()
g.pool.size = g.opts.PoolSize
g.pool.ttl = int64(g.opts.PoolTTL.Seconds())
g.pool.Unlock()
}
return nil
}
func (g *grpcClient) Options() client.Options {
return g.opts
}
func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
2019-07-07 14:44:09 +03:00
return newGRPCEvent(topic, msg, g.opts.ContentType, opts...)
2019-06-03 20:44:43 +03:00
}
func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...)
}
func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
if req == nil {
return errors.InternalServerError("go.micro.client", "req is nil")
} else if rsp == nil {
return errors.InternalServerError("go.micro.client", "rsp is nil")
}
2019-06-03 20:44:43 +03:00
// make a copy of call opts
callOpts := g.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// check if we already have a deadline
d, ok := ctx.Deadline()
if !ok {
// no deadline so we create a new one
2019-12-03 10:25:58 +03:00
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
defer cancel()
2019-06-03 20:44:43 +03:00
} else {
// got a deadline so no need to setup context
// but we need to set the timeout we pass along
opt := client.WithRequestTimeout(time.Until(d))
opt(&callOpts)
}
// should we noop right here?
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make copy of call method
gcall := g.call
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
gcall = callOpts.CallWrappers[i-1](gcall)
}
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = g.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = g.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(g.opts.Proxy) > 0 {
callOpts.Address = []string{g.opts.Proxy}
}
// lookup the route to send the reques to
// TODO apply any filtering here
routes, err := g.opts.Lookup(ctx, req, callOpts)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err := callOpts.Selector.Select(routes)
if err != nil {
return err
}
2019-06-03 20:44:43 +03:00
// return errors.New("go.micro.client", "request timeout", 408)
call := func(i int) error {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
// get the next node
node := next()
2019-06-03 20:44:43 +03:00
// make the call
err = gcall(ctx, node, req, rsp, callOpts)
// record the result of the call to inform future routing decisions
if verr := g.opts.Selector.Record(node, err); verr != nil {
return verr
}
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return verr
}
2019-06-03 20:44:43 +03:00
return err
}
ch := make(chan error, callOpts.Retries+1)
var gerr error
for i := 0; i <= callOpts.Retries; i++ {
2019-06-27 08:06:53 +03:00
go func(i int) {
2019-06-03 20:44:43 +03:00
ch <- call(i)
2019-06-27 08:06:53 +03:00
}(i)
2019-06-03 20:44:43 +03:00
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case err := <-ch:
// if the call succeeded lets bail early
if err == nil {
return nil
}
retry, rerr := callOpts.Retry(ctx, req, i, err)
if rerr != nil {
return rerr
}
if !retry {
return err
}
gerr = err
}
}
return gerr
}
func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
// make a copy of call opts
callOpts := g.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// #200 - streams shouldn't have a request timeout set on the context
// should we noop right here?
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make a copy of stream
gstream := g.stream
// wrap the call in reverse
for i := len(callOpts.CallWrappers); i > 0; i-- {
gstream = callOpts.CallWrappers[i-1](gstream)
}
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = g.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = g.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(g.opts.Proxy) > 0 {
callOpts.Address = []string{g.opts.Proxy}
}
// lookup the route to send the reques to
// TODO: move to internal lookup func
routes, err := g.opts.Lookup(ctx, req, callOpts)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// balance the list of nodes
next, err := callOpts.Selector.Select(routes)
if err != nil {
return nil, err
}
2019-06-03 20:44:43 +03:00
call := func(i int) (client.Stream, error) {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
// get the next node
node := next()
// make the call
stream := &grpcStream{}
err = g.stream(ctx, node, req, stream, callOpts)
// record the result of the call to inform future routing decisions
if verr := g.opts.Selector.Record(node, err); verr != nil {
return nil, verr
}
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return nil, verr
}
g.opts.Selector.Record(node, err)
2019-06-03 20:44:43 +03:00
return stream, err
}
type response struct {
stream client.Stream
err error
}
ch := make(chan response, callOpts.Retries+1)
var grr error
for i := 0; i <= callOpts.Retries; i++ {
2019-06-27 08:06:53 +03:00
go func(i int) {
2019-06-03 20:44:43 +03:00
s, err := call(i)
ch <- response{s, err}
2019-06-27 08:06:53 +03:00
}(i)
2019-06-03 20:44:43 +03:00
select {
case <-ctx.Done():
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case rsp := <-ch:
// if the call succeeded lets bail early
if rsp.err == nil {
return rsp.stream, nil
}
retry, rerr := callOpts.Retry(ctx, req, i, grr)
2019-06-03 20:44:43 +03:00
if rerr != nil {
return nil, rerr
}
if !retry {
return nil, rsp.err
}
grr = rsp.err
}
}
return nil, grr
}
func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
var options client.PublishOptions
var body []byte
// fail early on connect error
if !g.once.Load().(bool) {
if err := g.opts.Broker.Connect(); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
g.once.Store(true)
}
for _, o := range opts {
o(&options)
}
2019-06-03 20:44:43 +03:00
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(map[string]string)
}
md["Content-Type"] = p.ContentType()
md["Micro-Topic"] = p.Topic()
2019-06-03 20:44:43 +03:00
// passed in raw data
if d, ok := p.Payload().(*raw.Frame); ok {
body = d.Data
} else {
// use codec for payload
cf, err := g.newGRPCCodec(p.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
// set the body
b, err := cf.Marshal(p.Payload())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
body = b
2019-06-03 20:44:43 +03:00
}
topic := p.Topic()
// get the exchange
if len(options.Exchange) > 0 {
topic = options.Exchange
}
return g.opts.Broker.Publish(topic, &broker.Message{
2019-06-03 20:44:43 +03:00
Header: md,
Body: body,
}, broker.PublishContext(options.Context))
2019-06-03 20:44:43 +03:00
}
func (g *grpcClient) String() string {
return "grpc"
}
2019-10-09 09:45:51 +03:00
func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption {
2019-10-10 08:55:16 +03:00
if g.opts.CallOptions.Context == nil {
2019-10-09 09:45:51 +03:00
return nil
}
2019-10-10 08:55:16 +03:00
v := g.opts.CallOptions.Context.Value(grpcDialOptions{})
2019-10-09 09:45:51 +03:00
if v == nil {
return nil
}
opts, ok := v.([]grpc.DialOption)
if !ok {
return nil
}
return opts
}
func (g *grpcClient) getGrpcCallOptions() []grpc.CallOption {
if g.opts.CallOptions.Context == nil {
return nil
}
v := g.opts.CallOptions.Context.Value(grpcCallOptions{})
if v == nil {
return nil
}
opts, ok := v.([]grpc.CallOption)
if !ok {
return nil
}
return opts
}
2019-06-03 20:44:43 +03:00
func newClient(opts ...client.Option) client.Client {
options := client.NewOptions()
// default content type for grpc
options.ContentType = "application/grpc+proto"
2019-06-03 20:44:43 +03:00
for _, o := range opts {
o(&options)
}
rc := &grpcClient{
opts: options,
}
rc.once.Store(false)
rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams())
2019-06-03 20:44:43 +03:00
c := client.Client(rc)
// wrap in reverse
for i := len(options.Wrappers); i > 0; i-- {
c = options.Wrappers[i-1](c)
}
rc.codecs = make(map[string]encoding.Codec, len(defaultGRPCCodecs))
for k, v := range defaultGRPCCodecs {
rc.codecs[k] = v
}
var codecs map[string]encoding.Codec
if rc.opts.Context != nil {
if v := rc.opts.Context.Value(codecsKey{}); v != nil {
codecs = v.(map[string]encoding.Codec)
}
}
for k, v := range codecs {
rc.codecs[k] = v
}
2019-06-03 20:44:43 +03:00
return c
}
func NewClient(opts ...client.Option) client.Client {
return newClient(opts...)
}