github.com/annwntech/go-micro/v2@v2.9.5/client/grpc/grpc.go (about)

     1  // Package grpc provides a gRPC client
     2  package grpc
     3  
     4  import (
     5  	"context"
     6  	"crypto/tls"
     7  	"fmt"
     8  	"net"
     9  	"reflect"
    10  	"strings"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/annwntech/go-micro/v2/broker"
    15  	"github.com/annwntech/go-micro/v2/client"
    16  	"github.com/annwntech/go-micro/v2/client/selector"
    17  	raw "github.com/annwntech/go-micro/v2/codec/bytes"
    18  	"github.com/annwntech/go-micro/v2/errors"
    19  	"github.com/annwntech/go-micro/v2/metadata"
    20  	"github.com/annwntech/go-micro/v2/registry"
    21  	pnet "github.com/annwntech/go-micro/v2/util/net"
    22  
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/credentials"
    25  	"google.golang.org/grpc/encoding"
    26  	gmetadata "google.golang.org/grpc/metadata"
    27  )
    28  
    29  type grpcClient struct {
    30  	opts client.Options
    31  	pool *pool
    32  	once atomic.Value
    33  }
    34  
    35  func init() {
    36  	encoding.RegisterCodec(wrapCodec{jsonCodec{}})
    37  	encoding.RegisterCodec(wrapCodec{protoCodec{}})
    38  	encoding.RegisterCodec(wrapCodec{bytesCodec{}})
    39  }
    40  
    41  // secure returns the dial option for whether its a secure or insecure connection
    42  func (g *grpcClient) secure(addr string) grpc.DialOption {
    43  	// first we check if theres'a  tls config
    44  	if g.opts.Context != nil {
    45  		if v := g.opts.Context.Value(tlsAuth{}); v != nil {
    46  			tls := v.(*tls.Config)
    47  			creds := credentials.NewTLS(tls)
    48  			// return tls config if it exists
    49  			return grpc.WithTransportCredentials(creds)
    50  		}
    51  	}
    52  
    53  	// default config
    54  	tlsConfig := &tls.Config{}
    55  	defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
    56  
    57  	// check if the address is prepended with https
    58  	if strings.HasPrefix(addr, "https://") {
    59  		return defaultCreds
    60  	}
    61  
    62  	// if no port is specified or port is 443 default to tls
    63  	_, port, err := net.SplitHostPort(addr)
    64  	// assuming with no port its going to be secured
    65  	if port == "443" {
    66  		return defaultCreds
    67  	} else if err != nil && strings.Contains(err.Error(), "missing port in address") {
    68  		return defaultCreds
    69  	}
    70  
    71  	// other fallback to insecure
    72  	return grpc.WithInsecure()
    73  }
    74  
    75  func (g *grpcClient) next(request client.Request, opts client.CallOptions) (selector.Next, error) {
    76  	service, address, _ := pnet.Proxy(request.Service(), opts.Address)
    77  
    78  	// return remote address
    79  	if len(address) > 0 {
    80  		return func() (*registry.Node, error) {
    81  			return &registry.Node{
    82  				Address: address[0],
    83  			}, nil
    84  		}, nil
    85  	}
    86  
    87  	// get next nodes from the selector
    88  	next, err := g.opts.Selector.Select(service, opts.SelectOptions...)
    89  	if err != nil {
    90  		if err == selector.ErrNotFound {
    91  			return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
    92  		}
    93  		return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
    94  	}
    95  
    96  	return next, nil
    97  }
    98  
    99  func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
   100  	var header map[string]string
   101  
   102  	address := node.Address
   103  
   104  	header = make(map[string]string)
   105  	if md, ok := metadata.FromContext(ctx); ok {
   106  		header = make(map[string]string, len(md))
   107  		for k, v := range md {
   108  			header[strings.ToLower(k)] = v
   109  		}
   110  	} else {
   111  		header = make(map[string]string)
   112  	}
   113  
   114  	// set timeout in nanoseconds
   115  	header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout)
   116  	// set the content type for the request
   117  	header["x-content-type"] = req.ContentType()
   118  
   119  	md := gmetadata.New(header)
   120  	ctx = gmetadata.NewOutgoingContext(ctx, md)
   121  
   122  	cf, err := g.newGRPCCodec(req.ContentType())
   123  	if err != nil {
   124  		return errors.InternalServerError("go.micro.client", err.Error())
   125  	}
   126  
   127  	maxRecvMsgSize := g.maxRecvMsgSizeValue()
   128  	maxSendMsgSize := g.maxSendMsgSizeValue()
   129  
   130  	var grr error
   131  
   132  	grpcDialOptions := []grpc.DialOption{
   133  		grpc.WithTimeout(opts.DialTimeout),
   134  		g.secure(address),
   135  		grpc.WithDefaultCallOptions(
   136  			grpc.MaxCallRecvMsgSize(maxRecvMsgSize),
   137  			grpc.MaxCallSendMsgSize(maxSendMsgSize),
   138  		),
   139  	}
   140  
   141  	if opts := g.getGrpcDialOptions(); opts != nil {
   142  		grpcDialOptions = append(grpcDialOptions, opts...)
   143  	}
   144  
   145  	cc, err := g.pool.getConn(address, grpcDialOptions...)
   146  	if err != nil {
   147  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
   148  	}
   149  	defer func() {
   150  		// defer execution of release
   151  		g.pool.release(address, cc, grr)
   152  	}()
   153  
   154  	ch := make(chan error, 1)
   155  
   156  	go func() {
   157  		grpcCallOptions := []grpc.CallOption{
   158  			grpc.ForceCodec(cf),
   159  			grpc.CallContentSubtype(cf.Name())}
   160  		if opts := g.getGrpcCallOptions(); opts != nil {
   161  			grpcCallOptions = append(grpcCallOptions, opts...)
   162  		}
   163  		err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...)
   164  		ch <- microError(err)
   165  	}()
   166  
   167  	select {
   168  	case err := <-ch:
   169  		grr = err
   170  	case <-ctx.Done():
   171  		grr = errors.Timeout("go.micro.client", "%v", ctx.Err())
   172  	}
   173  
   174  	return grr
   175  }
   176  
   177  func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
   178  	var header map[string]string
   179  
   180  	address := node.Address
   181  
   182  	if md, ok := metadata.FromContext(ctx); ok {
   183  		header = make(map[string]string, len(md))
   184  		for k, v := range md {
   185  			header[k] = v
   186  		}
   187  	} else {
   188  		header = make(map[string]string)
   189  	}
   190  
   191  	// set timeout in nanoseconds
   192  	if opts.StreamTimeout > time.Duration(0) {
   193  		header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
   194  	}
   195  	// set the content type for the request
   196  	header["x-content-type"] = req.ContentType()
   197  
   198  	md := gmetadata.New(header)
   199  	ctx = gmetadata.NewOutgoingContext(ctx, md)
   200  
   201  	cf, err := g.newGRPCCodec(req.ContentType())
   202  	if err != nil {
   203  		return errors.InternalServerError("go.micro.client", err.Error())
   204  	}
   205  
   206  	var dialCtx context.Context
   207  	var cancel context.CancelFunc
   208  	if opts.DialTimeout >= 0 {
   209  		dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
   210  	} else {
   211  		dialCtx, cancel = context.WithCancel(ctx)
   212  	}
   213  	defer cancel()
   214  
   215  	wc := wrapCodec{cf}
   216  
   217  	grpcDialOptions := []grpc.DialOption{
   218  		grpc.WithTimeout(opts.DialTimeout),
   219  		g.secure(address),
   220  	}
   221  
   222  	if opts := g.getGrpcDialOptions(); opts != nil {
   223  		grpcDialOptions = append(grpcDialOptions, opts...)
   224  	}
   225  
   226  	cc, err := grpc.DialContext(dialCtx, address, grpcDialOptions...)
   227  	if err != nil {
   228  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
   229  	}
   230  
   231  	desc := &grpc.StreamDesc{
   232  		StreamName:    req.Service() + req.Endpoint(),
   233  		ClientStreams: true,
   234  		ServerStreams: true,
   235  	}
   236  
   237  	grpcCallOptions := []grpc.CallOption{
   238  		grpc.ForceCodec(wc),
   239  		grpc.CallContentSubtype(cf.Name()),
   240  	}
   241  	if opts := g.getGrpcCallOptions(); opts != nil {
   242  		grpcCallOptions = append(grpcCallOptions, opts...)
   243  	}
   244  
   245  	// create a new cancelling context
   246  	newCtx, cancel := context.WithCancel(ctx)
   247  
   248  	st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...)
   249  	if err != nil {
   250  		// we need to cleanup as we dialled and created a context
   251  		// cancel the context
   252  		cancel()
   253  		// close the connection
   254  		cc.Close()
   255  		// now return the error
   256  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err))
   257  	}
   258  
   259  	codec := &grpcCodec{
   260  		s: st,
   261  		c: wc,
   262  	}
   263  
   264  	// set request codec
   265  	if r, ok := req.(*grpcRequest); ok {
   266  		r.codec = codec
   267  	}
   268  
   269  	// setup the stream response
   270  	stream := &grpcStream{
   271  		context: ctx,
   272  		request: req,
   273  		response: &response{
   274  			conn:   cc,
   275  			stream: st,
   276  			codec:  cf,
   277  			gcodec: codec,
   278  		},
   279  		stream: st,
   280  		conn:   cc,
   281  		close: func(err error) {
   282  			// cancel the context if an error occured
   283  			if err != nil {
   284  				cancel()
   285  			}
   286  
   287  			// defer execution of release
   288  			// cc.Close()
   289  		},
   290  	}
   291  
   292  	// set the stream as the response
   293  	val := reflect.ValueOf(rsp).Elem()
   294  	val.Set(reflect.ValueOf(stream).Elem())
   295  	return nil
   296  }
   297  
   298  func (g *grpcClient) poolMaxStreams() int {
   299  	if g.opts.Context == nil {
   300  		return DefaultPoolMaxStreams
   301  	}
   302  	v := g.opts.Context.Value(poolMaxStreams{})
   303  	if v == nil {
   304  		return DefaultPoolMaxStreams
   305  	}
   306  	return v.(int)
   307  }
   308  
   309  func (g *grpcClient) poolMaxIdle() int {
   310  	if g.opts.Context == nil {
   311  		return DefaultPoolMaxIdle
   312  	}
   313  	v := g.opts.Context.Value(poolMaxIdle{})
   314  	if v == nil {
   315  		return DefaultPoolMaxIdle
   316  	}
   317  	return v.(int)
   318  }
   319  
   320  func (g *grpcClient) maxRecvMsgSizeValue() int {
   321  	if g.opts.Context == nil {
   322  		return DefaultMaxRecvMsgSize
   323  	}
   324  	v := g.opts.Context.Value(maxRecvMsgSizeKey{})
   325  	if v == nil {
   326  		return DefaultMaxRecvMsgSize
   327  	}
   328  	return v.(int)
   329  }
   330  
   331  func (g *grpcClient) maxSendMsgSizeValue() int {
   332  	if g.opts.Context == nil {
   333  		return DefaultMaxSendMsgSize
   334  	}
   335  	v := g.opts.Context.Value(maxSendMsgSizeKey{})
   336  	if v == nil {
   337  		return DefaultMaxSendMsgSize
   338  	}
   339  	return v.(int)
   340  }
   341  
   342  func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) {
   343  	codecs := make(map[string]encoding.Codec)
   344  	if g.opts.Context != nil {
   345  		if v := g.opts.Context.Value(codecsKey{}); v != nil {
   346  			codecs = v.(map[string]encoding.Codec)
   347  		}
   348  	}
   349  	if c, ok := codecs[contentType]; ok {
   350  		return wrapCodec{c}, nil
   351  	}
   352  	if c, ok := defaultGRPCCodecs[contentType]; ok {
   353  		return wrapCodec{c}, nil
   354  	}
   355  	return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
   356  }
   357  
   358  func (g *grpcClient) Init(opts ...client.Option) error {
   359  	size := g.opts.PoolSize
   360  	ttl := g.opts.PoolTTL
   361  
   362  	for _, o := range opts {
   363  		o(&g.opts)
   364  	}
   365  
   366  	// update pool configuration if the options changed
   367  	if size != g.opts.PoolSize || ttl != g.opts.PoolTTL {
   368  		g.pool.Lock()
   369  		g.pool.size = g.opts.PoolSize
   370  		g.pool.ttl = int64(g.opts.PoolTTL.Seconds())
   371  		g.pool.Unlock()
   372  	}
   373  
   374  	return nil
   375  }
   376  
   377  func (g *grpcClient) Options() client.Options {
   378  	return g.opts
   379  }
   380  
   381  func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
   382  	return newGRPCEvent(topic, msg, g.opts.ContentType, opts...)
   383  }
   384  
   385  func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
   386  	return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...)
   387  }
   388  
   389  func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
   390  	if req == nil {
   391  		return errors.InternalServerError("go.micro.client", "req is nil")
   392  	} else if rsp == nil {
   393  		return errors.InternalServerError("go.micro.client", "rsp is nil")
   394  	}
   395  	// make a copy of call opts
   396  	callOpts := g.opts.CallOptions
   397  	for _, opt := range opts {
   398  		opt(&callOpts)
   399  	}
   400  
   401  	next, err := g.next(req, callOpts)
   402  	if err != nil {
   403  		return err
   404  	}
   405  
   406  	// check if we already have a deadline
   407  	d, ok := ctx.Deadline()
   408  	if !ok {
   409  		// no deadline so we create a new one
   410  		var cancel context.CancelFunc
   411  		ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
   412  		defer cancel()
   413  	} else {
   414  		// got a deadline so no need to setup context
   415  		// but we need to set the timeout we pass along
   416  		opt := client.WithRequestTimeout(time.Until(d))
   417  		opt(&callOpts)
   418  	}
   419  
   420  	// should we noop right here?
   421  	select {
   422  	case <-ctx.Done():
   423  		return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   424  	default:
   425  	}
   426  
   427  	// make copy of call method
   428  	gcall := g.call
   429  
   430  	// wrap the call in reverse
   431  	for i := len(callOpts.CallWrappers); i > 0; i-- {
   432  		gcall = callOpts.CallWrappers[i-1](gcall)
   433  	}
   434  
   435  	// return errors.New("go.micro.client", "request timeout", 408)
   436  	call := func(i int) error {
   437  		// call backoff first. Someone may want an initial start delay
   438  		t, err := callOpts.Backoff(ctx, req, i)
   439  		if err != nil {
   440  			return errors.InternalServerError("go.micro.client", err.Error())
   441  		}
   442  
   443  		// only sleep if greater than 0
   444  		if t.Seconds() > 0 {
   445  			time.Sleep(t)
   446  		}
   447  
   448  		// select next node
   449  		node, err := next()
   450  		service := req.Service()
   451  		if err != nil {
   452  			if err == selector.ErrNotFound {
   453  				return errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
   454  			}
   455  			return errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
   456  		}
   457  
   458  		// make the call
   459  		err = gcall(ctx, node, req, rsp, callOpts)
   460  		g.opts.Selector.Mark(service, node, err)
   461  		if verr, ok := err.(*errors.Error); ok {
   462  			return verr
   463  		}
   464  
   465  		return err
   466  	}
   467  
   468  	ch := make(chan error, callOpts.Retries+1)
   469  	var gerr error
   470  
   471  	for i := 0; i <= callOpts.Retries; i++ {
   472  		go func(i int) {
   473  			ch <- call(i)
   474  		}(i)
   475  
   476  		select {
   477  		case <-ctx.Done():
   478  			return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   479  		case err := <-ch:
   480  			// if the call succeeded lets bail early
   481  			if err == nil {
   482  				return nil
   483  			}
   484  
   485  			retry, rerr := callOpts.Retry(ctx, req, i, err)
   486  			if rerr != nil {
   487  				return rerr
   488  			}
   489  
   490  			if !retry {
   491  				return err
   492  			}
   493  
   494  			gerr = err
   495  		}
   496  	}
   497  
   498  	return gerr
   499  }
   500  
   501  func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
   502  	// make a copy of call opts
   503  	callOpts := g.opts.CallOptions
   504  	for _, opt := range opts {
   505  		opt(&callOpts)
   506  	}
   507  
   508  	next, err := g.next(req, callOpts)
   509  	if err != nil {
   510  		return nil, err
   511  	}
   512  
   513  	// #200 - streams shouldn't have a request timeout set on the context
   514  
   515  	// should we noop right here?
   516  	select {
   517  	case <-ctx.Done():
   518  		return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   519  	default:
   520  	}
   521  
   522  	// make a copy of stream
   523  	gstream := g.stream
   524  
   525  	// wrap the call in reverse
   526  	for i := len(callOpts.CallWrappers); i > 0; i-- {
   527  		gstream = callOpts.CallWrappers[i-1](gstream)
   528  	}
   529  
   530  	call := func(i int) (client.Stream, error) {
   531  		// call backoff first. Someone may want an initial start delay
   532  		t, err := callOpts.Backoff(ctx, req, i)
   533  		if err != nil {
   534  			return nil, errors.InternalServerError("go.micro.client", err.Error())
   535  		}
   536  
   537  		// only sleep if greater than 0
   538  		if t.Seconds() > 0 {
   539  			time.Sleep(t)
   540  		}
   541  
   542  		node, err := next()
   543  		service := req.Service()
   544  		if err != nil {
   545  			if err == selector.ErrNotFound {
   546  				return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
   547  			}
   548  			return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
   549  		}
   550  
   551  		// make the call
   552  		stream := &grpcStream{}
   553  		err = g.stream(ctx, node, req, stream, callOpts)
   554  
   555  		g.opts.Selector.Mark(service, node, err)
   556  		return stream, err
   557  	}
   558  
   559  	type response struct {
   560  		stream client.Stream
   561  		err    error
   562  	}
   563  
   564  	ch := make(chan response, callOpts.Retries+1)
   565  	var grr error
   566  
   567  	for i := 0; i <= callOpts.Retries; i++ {
   568  		go func(i int) {
   569  			s, err := call(i)
   570  			ch <- response{s, err}
   571  		}(i)
   572  
   573  		select {
   574  		case <-ctx.Done():
   575  			return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   576  		case rsp := <-ch:
   577  			// if the call succeeded lets bail early
   578  			if rsp.err == nil {
   579  				return rsp.stream, nil
   580  			}
   581  
   582  			retry, rerr := callOpts.Retry(ctx, req, i, err)
   583  			if rerr != nil {
   584  				return nil, rerr
   585  			}
   586  
   587  			if !retry {
   588  				return nil, rsp.err
   589  			}
   590  
   591  			grr = rsp.err
   592  		}
   593  	}
   594  
   595  	return nil, grr
   596  }
   597  
   598  func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
   599  	var options client.PublishOptions
   600  	for _, o := range opts {
   601  		o(&options)
   602  	}
   603  
   604  	md, ok := metadata.FromContext(ctx)
   605  	if !ok {
   606  		md = make(map[string]string)
   607  	}
   608  	md["Content-Type"] = p.ContentType()
   609  	md["Micro-Topic"] = p.Topic()
   610  
   611  	cf, err := g.newGRPCCodec(p.ContentType())
   612  	if err != nil {
   613  		return errors.InternalServerError("go.micro.client", err.Error())
   614  	}
   615  
   616  	var body []byte
   617  
   618  	// passed in raw data
   619  	if d, ok := p.Payload().(*raw.Frame); ok {
   620  		body = d.Data
   621  	} else {
   622  		// set the body
   623  		b, err := cf.Marshal(p.Payload())
   624  		if err != nil {
   625  			return errors.InternalServerError("go.micro.client", err.Error())
   626  		}
   627  		body = b
   628  	}
   629  
   630  	if !g.once.Load().(bool) {
   631  		if err = g.opts.Broker.Connect(); err != nil {
   632  			return errors.InternalServerError("go.micro.client", err.Error())
   633  		}
   634  		g.once.Store(true)
   635  	}
   636  
   637  	topic := p.Topic()
   638  
   639  	// get the exchange
   640  	if len(options.Exchange) > 0 {
   641  		topic = options.Exchange
   642  	}
   643  
   644  	return g.opts.Broker.Publish(topic, &broker.Message{
   645  		Header: md,
   646  		Body:   body,
   647  	}, broker.PublishContext(options.Context))
   648  }
   649  
   650  func (g *grpcClient) String() string {
   651  	return "grpc"
   652  }
   653  
   654  func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption {
   655  	if g.opts.CallOptions.Context == nil {
   656  		return nil
   657  	}
   658  
   659  	v := g.opts.CallOptions.Context.Value(grpcDialOptions{})
   660  
   661  	if v == nil {
   662  		return nil
   663  	}
   664  
   665  	opts, ok := v.([]grpc.DialOption)
   666  
   667  	if !ok {
   668  		return nil
   669  	}
   670  
   671  	return opts
   672  }
   673  
   674  func (g *grpcClient) getGrpcCallOptions() []grpc.CallOption {
   675  	if g.opts.CallOptions.Context == nil {
   676  		return nil
   677  	}
   678  
   679  	v := g.opts.CallOptions.Context.Value(grpcCallOptions{})
   680  
   681  	if v == nil {
   682  		return nil
   683  	}
   684  
   685  	opts, ok := v.([]grpc.CallOption)
   686  
   687  	if !ok {
   688  		return nil
   689  	}
   690  
   691  	return opts
   692  }
   693  
   694  func newClient(opts ...client.Option) client.Client {
   695  	options := client.NewOptions()
   696  	// default content type for grpc
   697  	options.ContentType = "application/grpc+proto"
   698  
   699  	for _, o := range opts {
   700  		o(&options)
   701  	}
   702  
   703  	rc := &grpcClient{
   704  		opts: options,
   705  	}
   706  	rc.once.Store(false)
   707  
   708  	rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams())
   709  
   710  	c := client.Client(rc)
   711  
   712  	// wrap in reverse
   713  	for i := len(options.Wrappers); i > 0; i-- {
   714  		c = options.Wrappers[i-1](c)
   715  	}
   716  
   717  	return c
   718  }
   719  
   720  func NewClient(opts ...client.Option) client.Client {
   721  	return newClient(opts...)
   722  }