go.uber.org/yarpc@v1.72.1/encoding/thrift/outbound_nowire.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 thrift
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"fmt"
    27  
    28  	"go.uber.org/thriftrw/protocol/binary"
    29  	"go.uber.org/thriftrw/protocol/stream"
    30  	"go.uber.org/thriftrw/wire"
    31  	"go.uber.org/yarpc"
    32  	encodingapi "go.uber.org/yarpc/api/encoding"
    33  	"go.uber.org/yarpc/api/transport"
    34  	"go.uber.org/yarpc/encoding/thrift/internal"
    35  	"go.uber.org/yarpc/pkg/encoding"
    36  	"go.uber.org/yarpc/pkg/errors"
    37  	"go.uber.org/yarpc/pkg/procedure"
    38  )
    39  
    40  // NoWireClient is a generic Thrift client for encoding/decoding using
    41  // ThriftRW's "streaming" mechanisms. The body of the provided request
    42  // ('reqBody') will be written out through its method 'Encode',
    43  // while the body of the response ('resBody') is read out through its method
    44  // 'Decode'.
    45  // It speaks in raw Thrift payloads.
    46  //
    47  // Users should use the client generated by the code generator rather than
    48  // using this directly.
    49  type NoWireClient interface {
    50  	// Call the given Thrift method.
    51  	Call(ctx context.Context, reqBody stream.Enveloper, resBody stream.BodyReader, opts ...yarpc.CallOption) error
    52  	CallOneway(ctx context.Context, reqBody stream.Enveloper, opts ...yarpc.CallOption) (transport.Ack, error)
    53  
    54  	// Enabled returns whether or not this client is enabled through a
    55  	// ClientOption. This ClientOption is toggled through the 'NoWire(bool)'
    56  	// option.
    57  	Enabled() bool
    58  }
    59  
    60  // NewNoWire creates a new Thrift client that leverages ThriftRW's "streaming"
    61  // implementation.
    62  func NewNoWire(c Config, opts ...ClientOption) NoWireClient {
    63  	// Code generated for Thrift client instantiation will probably be something
    64  	// like this:
    65  	//
    66  	// 	func New(cc transport.ClientConfig, opts ...ClientOption) *MyServiceClient {
    67  	// 		c := thrift.NewNoWire(thrift.Config{
    68  	// 			Service: "MyService",
    69  	// 			ClientConfig: cc,
    70  	// 			Protocol: binary.Default,
    71  	// 		}, opts...)
    72  	// 		return &MyServiceClient{client: c}
    73  	// 	}
    74  	//
    75  	// So Config is really the internal config as far as consumers of the
    76  	// generated client are concerned.
    77  
    78  	// default NoWire to true because this is the our final state to achieve
    79  	// but we still allow users to opt out by overriding NoWire to false.
    80  	cc := clientConfig{NoWire: true}
    81  	for _, opt := range opts {
    82  		opt.applyClientOption(&cc)
    83  	}
    84  
    85  	var p stream.Protocol = binary.Default
    86  	if cc.Protocol != nil {
    87  		if val, ok := cc.Protocol.(stream.Protocol); ok {
    88  			p = val
    89  		} else {
    90  			panic(fmt.Sprintf(
    91  				"Protocol config option provided, NewNoWire expects provided protocol %T to implement stream.Protocol", cc.Protocol))
    92  		}
    93  	}
    94  
    95  	svc := c.Service
    96  	if cc.ServiceName != "" {
    97  		svc = cc.ServiceName
    98  	}
    99  
   100  	if cc.Multiplexed {
   101  		p = multiplexedOutboundNoWireProtocol{
   102  			Protocol: p,
   103  			Service:  svc,
   104  		}
   105  	}
   106  
   107  	return noWireThriftClient{
   108  		p:             p,
   109  		cc:            c.ClientConfig,
   110  		thriftService: svc,
   111  		Enveloping:    cc.Enveloping,
   112  		NoWire:        cc.NoWire,
   113  	}
   114  }
   115  
   116  type noWireThriftClient struct {
   117  	cc transport.ClientConfig
   118  	p  stream.Protocol
   119  
   120  	// name of the Thrift service
   121  	thriftService string
   122  	Enveloping    bool
   123  	NoWire        bool
   124  }
   125  
   126  func (c noWireThriftClient) Call(ctx context.Context, reqBody stream.Enveloper, resBody stream.BodyReader, opts ...yarpc.CallOption) error {
   127  	// Code generated for Thrift client calls will probably be something like
   128  	// this:
   129  	//
   130  	// 	func (c *MyServiceClient) someMethod(ctx context.Context, arg1 Arg1Type, arg2 arg2Type, opts ...yarpc.CallOption) (returnValue, error) {
   131  	// 		var result myservice.SomeMethodResult
   132  	// 		args := myservice.SomeMethodHelper.Args(arg1, arg2)
   133  	// 		err := c.client.Call(ctx, args, result, opts...)
   134  	//
   135  	// 		success, err := myservice.SomeMethodHelper.UnwrapResponse(&result)
   136  	// 		return success, err
   137  	// 	}
   138  
   139  	out := c.cc.GetUnaryOutbound()
   140  
   141  	treq, proto, err := c.buildTransportRequest(reqBody)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	call := encodingapi.NewOutboundCall(encoding.FromOptions(opts)...)
   147  	ctx, err = call.WriteToRequest(ctx, treq)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	tres, err := out.Call(ctx, treq)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	defer tres.Body.Close()
   157  
   158  	if _, err := call.ReadFromResponse(ctx, tres); err != nil {
   159  		return err
   160  	}
   161  
   162  	sr := proto.Reader(tres.Body)
   163  	defer sr.Close()
   164  
   165  	envelope, err := sr.ReadEnvelopeBegin()
   166  	if err != nil {
   167  		return errors.ResponseBodyDecodeError(treq, err)
   168  	}
   169  
   170  	switch envelope.Type {
   171  	case wire.Reply:
   172  		if err := resBody.Decode(sr); err != nil {
   173  			return err
   174  		}
   175  		return sr.ReadEnvelopeEnd()
   176  	case wire.Exception:
   177  		var exc internal.TApplicationException
   178  		if err := exc.Decode(sr); err != nil {
   179  			return errors.ResponseBodyDecodeError(treq, err)
   180  		}
   181  		defer func() {
   182  			_ = sr.ReadEnvelopeEnd()
   183  		}()
   184  
   185  		return thriftException{
   186  			Service:   treq.Service,
   187  			Procedure: treq.Procedure,
   188  			Reason:    &exc,
   189  		}
   190  	default:
   191  		return errors.ResponseBodyDecodeError(
   192  			treq, errUnexpectedEnvelopeType(envelope.Type))
   193  	}
   194  }
   195  
   196  func (c noWireThriftClient) CallOneway(ctx context.Context, reqBody stream.Enveloper, opts ...yarpc.CallOption) (transport.Ack, error) {
   197  	out := c.cc.GetOnewayOutbound()
   198  
   199  	treq, _, err := c.buildTransportRequest(reqBody)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	call := encodingapi.NewOutboundCall(encoding.FromOptions(opts)...)
   205  	ctx, err = call.WriteToRequest(ctx, treq)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	return out.CallOneway(ctx, treq)
   211  }
   212  
   213  func (c noWireThriftClient) Enabled() bool {
   214  	return c.NoWire
   215  }
   216  
   217  func (c noWireThriftClient) buildTransportRequest(reqBody stream.Enveloper) (*transport.Request, stream.Protocol, error) {
   218  	proto := c.p
   219  	if !c.Enveloping {
   220  		proto = disableEnvelopingNoWireProtocol{
   221  			Protocol: proto,
   222  			Type:     wire.Reply, // we only decode replies with this instance
   223  		}
   224  	}
   225  
   226  	treq := transport.Request{
   227  		Caller:    c.cc.Caller(),
   228  		Service:   c.cc.Service(),
   229  		Encoding:  Encoding,
   230  		Procedure: procedure.ToName(c.thriftService, reqBody.MethodName()),
   231  	}
   232  
   233  	envType := reqBody.EnvelopeType()
   234  	if envType != wire.Call && envType != wire.OneWay {
   235  		return nil, nil, errors.RequestBodyEncodeError(
   236  			&treq, errUnexpectedEnvelopeType(envType),
   237  		)
   238  	}
   239  
   240  	var buffer bytes.Buffer
   241  	sw := proto.Writer(&buffer)
   242  	defer sw.Close()
   243  
   244  	if err := sw.WriteEnvelopeBegin(stream.EnvelopeHeader{
   245  		Name:  reqBody.MethodName(),
   246  		Type:  envType,
   247  		SeqID: 1, // don't care
   248  	}); err != nil {
   249  		return nil, nil, errors.RequestBodyEncodeError(&treq, err)
   250  	}
   251  
   252  	if err := reqBody.Encode(sw); err != nil {
   253  		return nil, nil, errors.RequestBodyEncodeError(&treq, err)
   254  	}
   255  
   256  	if err := sw.WriteEnvelopeEnd(); err != nil {
   257  		return nil, nil, errors.RequestBodyEncodeError(&treq, err)
   258  	}
   259  
   260  	treq.Body = &buffer
   261  	treq.BodySize = buffer.Len()
   262  	return &treq, proto, nil
   263  }