go.uber.org/yarpc@v1.72.1/encoding/protobuf/v2/outbound.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package v2
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  
    27  	"go.uber.org/yarpc"
    28  	apiencoding "go.uber.org/yarpc/api/encoding"
    29  	"go.uber.org/yarpc/api/transport"
    30  	"go.uber.org/yarpc/pkg/encoding"
    31  	"go.uber.org/yarpc/pkg/errors"
    32  	"go.uber.org/yarpc/pkg/procedure"
    33  	"go.uber.org/yarpc/yarpcerrors"
    34  	"google.golang.org/protobuf/proto"
    35  )
    36  
    37  type client struct {
    38  	serviceName    string
    39  	outboundConfig *transport.OutboundConfig
    40  	encoding       transport.Encoding
    41  	codec          *codec
    42  }
    43  
    44  func newClient(serviceName string, clientConfig transport.ClientConfig, anyResolver AnyResolver, options ...ClientOption) *client {
    45  	outboundConfig := toOutboundConfig(clientConfig)
    46  	client := &client{
    47  		serviceName:    serviceName,
    48  		outboundConfig: outboundConfig,
    49  		encoding:       Encoding,
    50  		codec:          newCodec(anyResolver),
    51  	}
    52  	for _, option := range options {
    53  		option.apply(client)
    54  	}
    55  	return client
    56  }
    57  
    58  func toOutboundConfig(cc transport.ClientConfig) *transport.OutboundConfig {
    59  	if outboundConfig, ok := cc.(*transport.OutboundConfig); ok {
    60  		return outboundConfig
    61  	}
    62  	// If the config is not an *OutboundConfig we assume the only Outbound is
    63  	// unary and create our own outbound config.
    64  	// If there is no unary outbound, this function will panic, but, we're kinda
    65  	// stuck with that. (and why the hell are you passing a oneway-only client
    66  	// config to protobuf anyway?).
    67  	return &transport.OutboundConfig{
    68  		CallerName: cc.Caller(),
    69  		Outbounds: transport.Outbounds{
    70  			ServiceName: cc.Service(),
    71  			Unary:       cc.GetUnaryOutbound(),
    72  		},
    73  	}
    74  }
    75  
    76  func (c *client) Call(
    77  	ctx context.Context,
    78  	requestMethodName string,
    79  	request proto.Message,
    80  	newResponse func() proto.Message,
    81  	options ...yarpc.CallOption,
    82  ) (proto.Message, error) {
    83  	ctx, call, transportRequest, cleanup, err := c.buildTransportRequest(ctx, requestMethodName, request, options)
    84  	if cleanup != nil {
    85  		defer cleanup()
    86  	}
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	unaryOutbound := c.outboundConfig.Outbounds.Unary
    91  	if unaryOutbound == nil {
    92  		return nil, yarpcerrors.InternalErrorf("no unary outbounds for OutboundConfig %s", c.outboundConfig.CallerName)
    93  	}
    94  	transportResponse, appErr := unaryOutbound.Call(ctx, transportRequest)
    95  	appErr = convertFromYARPCError(transportRequest.Encoding, appErr, c.codec)
    96  	if transportResponse == nil {
    97  		return nil, appErr
    98  	}
    99  	if transportResponse.Body != nil {
   100  		// thrift is not checking the error, should be consistent
   101  		defer transportResponse.Body.Close()
   102  	}
   103  	if _, err := call.ReadFromResponse(ctx, transportResponse); err != nil {
   104  		return nil, err
   105  	}
   106  	var response proto.Message
   107  	if transportResponse.Body != nil {
   108  		response = newResponse()
   109  		if err := unmarshal(transportRequest.Encoding, transportResponse.Body, response, c.codec); err != nil {
   110  			return nil, errors.ResponseBodyDecodeError(transportRequest, err)
   111  		}
   112  	}
   113  	return response, appErr
   114  }
   115  
   116  func (c *client) CallOneway(
   117  	ctx context.Context,
   118  	requestMethodName string,
   119  	request proto.Message,
   120  	options ...yarpc.CallOption,
   121  ) (transport.Ack, error) {
   122  	ctx, _, transportRequest, cleanup, err := c.buildTransportRequest(ctx, requestMethodName, request, options)
   123  	if cleanup != nil {
   124  		defer cleanup()
   125  	}
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	onewayOutbound := c.outboundConfig.Outbounds.Oneway
   130  	if onewayOutbound == nil {
   131  		return nil, yarpcerrors.InternalErrorf("no oneway outbounds for OutboundConfig %s", c.outboundConfig.CallerName)
   132  	}
   133  	ack, err := onewayOutbound.CallOneway(ctx, transportRequest)
   134  	return ack, convertFromYARPCError(transportRequest.Encoding, err, c.codec)
   135  }
   136  
   137  func (c *client) buildTransportRequest(ctx context.Context, requestMethodName string, request proto.Message, options []yarpc.CallOption) (context.Context, *apiencoding.OutboundCall, *transport.Request, func(), error) {
   138  	transportRequest := &transport.Request{
   139  		Caller:    c.outboundConfig.CallerName,
   140  		Service:   c.outboundConfig.Outbounds.ServiceName,
   141  		Procedure: procedure.ToName(c.serviceName, requestMethodName),
   142  		Encoding:  c.encoding,
   143  	}
   144  	call := apiencoding.NewOutboundCall(encoding.FromOptions(options)...)
   145  	ctx, err := call.WriteToRequest(ctx, transportRequest)
   146  	if err != nil {
   147  		return nil, nil, nil, nil, err
   148  	}
   149  	if transportRequest.Encoding != Encoding && transportRequest.Encoding != JSONEncoding {
   150  		return nil, nil, nil, nil, yarpcerrors.Newf(yarpcerrors.CodeInternal, "can only use encodings %q or %q, but %q was specified", Encoding, JSONEncoding, transportRequest.Encoding)
   151  	}
   152  	if request != nil {
   153  		requestData, cleanup, err := marshal(transportRequest.Encoding, request, c.codec)
   154  		if err != nil {
   155  			return nil, nil, nil, cleanup, errors.RequestBodyEncodeError(transportRequest, err)
   156  		}
   157  		if requestData != nil {
   158  			transportRequest.Body = bytes.NewReader(requestData)
   159  			transportRequest.BodySize = len(requestData)
   160  		}
   161  		return ctx, call, transportRequest, cleanup, nil
   162  	}
   163  	return ctx, call, transportRequest, nil, nil
   164  }
   165  
   166  func (c *client) CallStream(
   167  	ctx context.Context,
   168  	requestMethodName string,
   169  	opts ...yarpc.CallOption,
   170  ) (*ClientStream, error) {
   171  	streamRequest := &transport.StreamRequest{
   172  		Meta: &transport.RequestMeta{
   173  			Caller:    c.outboundConfig.CallerName,
   174  			Service:   c.outboundConfig.Outbounds.ServiceName,
   175  			Procedure: procedure.ToName(c.serviceName, requestMethodName),
   176  			Encoding:  c.encoding,
   177  		},
   178  	}
   179  	call, err := apiencoding.NewStreamOutboundCall(encoding.FromOptions(opts)...)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	ctx, err = call.WriteToRequestMeta(ctx, streamRequest.Meta)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	if streamRequest.Meta.Encoding != Encoding && streamRequest.Meta.Encoding != JSONEncoding {
   188  		return nil, yarpcerrors.InternalErrorf("can only use encodings %q or %q, but %q was specified", Encoding, JSONEncoding, streamRequest.Meta.Encoding)
   189  	}
   190  	streamOutbound := c.outboundConfig.Outbounds.Stream
   191  	if streamOutbound == nil {
   192  		return nil, yarpcerrors.InternalErrorf("no stream outbounds for OutboundConfig %s", c.outboundConfig.CallerName)
   193  	}
   194  	stream, err := streamOutbound.CallStream(ctx, streamRequest)
   195  	if err != nil {
   196  		return nil, convertFromYARPCError(streamRequest.Meta.Encoding, err, c.codec)
   197  	}
   198  	return &ClientStream{stream: stream, codec: c.codec}, nil
   199  }