go.uber.org/yarpc@v1.72.1/transport/tchannel/channel_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 tchannel
    22  
    23  import (
    24  	"context"
    25  
    26  	"github.com/uber/tchannel-go"
    27  	"go.uber.org/yarpc/api/transport"
    28  	"go.uber.org/yarpc/api/x/introspection"
    29  	intyarpcerrors "go.uber.org/yarpc/internal/yarpcerrors"
    30  	"go.uber.org/yarpc/pkg/errors"
    31  	"go.uber.org/yarpc/pkg/lifecycle"
    32  	"go.uber.org/yarpc/yarpcerrors"
    33  )
    34  
    35  var (
    36  	_ transport.UnaryOutbound              = (*ChannelOutbound)(nil)
    37  	_ introspection.IntrospectableOutbound = (*ChannelOutbound)(nil)
    38  )
    39  
    40  // NewOutbound builds a new TChannel outbound using the transport's shared
    41  // channel to make requests to any connected peer.
    42  func (t *ChannelTransport) NewOutbound() *ChannelOutbound {
    43  	return &ChannelOutbound{
    44  		once:      lifecycle.NewOnce(),
    45  		channel:   t.ch,
    46  		transport: t,
    47  	}
    48  }
    49  
    50  // NewSingleOutbound builds a new TChannel outbound using the transport's shared
    51  // channel to a specific peer.
    52  func (t *ChannelTransport) NewSingleOutbound(addr string) *ChannelOutbound {
    53  	return &ChannelOutbound{
    54  		once:      lifecycle.NewOnce(),
    55  		channel:   t.ch,
    56  		transport: t,
    57  		addr:      addr,
    58  	}
    59  }
    60  
    61  // ChannelOutbound sends YARPC requests over TChannel. It may be constructed
    62  // using the NewOutbound or NewSingleOutbound methods on the
    63  // tchannel.ChannelTransport.
    64  // If you have a YARPC peer.Chooser, use the unqualified tchannel.Transport
    65  // instead (instead of the tchannel.ChannelTransport).
    66  type ChannelOutbound struct {
    67  	channel   Channel
    68  	transport *ChannelTransport
    69  
    70  	// If specified, this is the address to which requests will be made.
    71  	// Otherwise, the global peer list of the Channel will be used.
    72  	addr string
    73  
    74  	once *lifecycle.Once
    75  }
    76  
    77  // Transports returns the underlying TChannel Transport for this outbound.
    78  func (o *ChannelOutbound) Transports() []transport.Transport {
    79  	return []transport.Transport{o.transport}
    80  }
    81  
    82  // Start starts the TChannel outbound.
    83  func (o *ChannelOutbound) Start() error {
    84  	// TODO: Should we create the connection to HostPort (if specified) here or
    85  	// wait for the first call?
    86  	return o.once.Start(nil)
    87  }
    88  
    89  // Stop stops the TChannel outbound.
    90  func (o *ChannelOutbound) Stop() error {
    91  	return o.once.Stop(o.stop)
    92  }
    93  
    94  func (o *ChannelOutbound) stop() error {
    95  	o.channel.Close()
    96  	return nil
    97  }
    98  
    99  // IsRunning returns whether the ChannelOutbound is running.
   100  func (o *ChannelOutbound) IsRunning() bool {
   101  	return o.once.IsRunning()
   102  }
   103  
   104  // Call sends an RPC over this TChannel outbound.
   105  func (o *ChannelOutbound) Call(ctx context.Context, req *transport.Request) (*transport.Response, error) {
   106  	if req == nil {
   107  		return nil, yarpcerrors.InvalidArgumentErrorf("request for tchannel channel outbound was nil")
   108  	}
   109  	if err := o.once.WaitUntilRunning(ctx); err != nil {
   110  		return nil, intyarpcerrors.AnnotateWithInfo(yarpcerrors.FromError(err), "error waiting for tchannel channel outbound to start for service: %s", req.Service)
   111  	}
   112  	if _, ok := ctx.(tchannel.ContextWithHeaders); ok {
   113  		return nil, errDoNotUseContextWithHeaders
   114  	}
   115  
   116  	// NB(abg): Under the current API, the local service's name is required
   117  	// twice: once when constructing the TChannel and then again when
   118  	// constructing the RPC.
   119  	var call *tchannel.OutboundCall
   120  	var err error
   121  
   122  	format := tchannel.Format(req.Encoding)
   123  	callOptions := tchannel.CallOptions{
   124  		Format:          format,
   125  		ShardKey:        req.ShardKey,
   126  		RoutingKey:      req.RoutingKey,
   127  		RoutingDelegate: req.RoutingDelegate,
   128  	}
   129  	if o.addr != "" {
   130  		// If the hostport is given, we use the BeginCall on the channel
   131  		// instead of the subchannel.
   132  		call, err = o.channel.BeginCall(
   133  			// TODO(abg): Set TimeoutPerAttempt in the context's retry options if
   134  			// TTL is set.
   135  			// (kris): Consider instead moving TimeoutPerAttempt to an outer
   136  			// layer, just clamp the context on outbound call.
   137  			ctx,
   138  			o.addr,
   139  			req.Service,
   140  			req.Procedure,
   141  			&callOptions,
   142  		)
   143  	} else {
   144  		call, err = o.channel.GetSubChannel(req.Service).BeginCall(
   145  			// TODO(abg): Set TimeoutPerAttempt in the context's retry options if
   146  			// TTL is set.
   147  			ctx,
   148  			req.Procedure,
   149  			&callOptions,
   150  		)
   151  	}
   152  
   153  	if err != nil {
   154  		return nil, toYARPCError(req, err)
   155  	}
   156  
   157  	reqHeaders := req.Headers.Items()
   158  	if o.transport.originalHeaders {
   159  		reqHeaders = req.Headers.OriginalItems()
   160  	}
   161  	// baggage headers are transport implementation details that are stripped out (and stored in the context). Users don't interact with it
   162  	tracingBaggage := tchannel.InjectOutboundSpan(call.Response(), nil)
   163  	if err := writeHeaders(format, reqHeaders, tracingBaggage, call.Arg2Writer); err != nil {
   164  		// TODO(abg): This will wrap IO errors while writing headers as encode
   165  		// errors. We should fix that.
   166  		return nil, errors.RequestHeadersEncodeError(req, err)
   167  	}
   168  
   169  	if err := writeBody(req.Body, call); err != nil {
   170  		return nil, toYARPCError(req, err)
   171  	}
   172  
   173  	res := call.Response()
   174  	headers, err := readHeaders(format, res.Arg2Reader)
   175  	if err != nil {
   176  		if err, ok := err.(tchannel.SystemError); ok {
   177  			return nil, fromSystemError(err)
   178  		}
   179  		// TODO(abg): This will wrap IO errors while reading headers as decode
   180  		// errors. We should fix that.
   181  		return nil, errors.ResponseHeadersDecodeError(req, err)
   182  	}
   183  
   184  	resBody, err := res.Arg3Reader()
   185  	if err != nil {
   186  		if err, ok := err.(tchannel.SystemError); ok {
   187  			return nil, fromSystemError(err)
   188  		}
   189  		return nil, toYARPCError(req, err)
   190  	}
   191  
   192  	respService, _ := headers.Get(ServiceHeaderKey) // validateServiceName handles empty strings
   193  	if err := validateServiceName(req.Service, respService); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	err = getResponseError(headers)
   198  	deleteReservedHeaders(headers)
   199  
   200  	resp := &transport.Response{
   201  		Headers:          headers,
   202  		Body:             resBody,
   203  		ApplicationError: res.ApplicationError(),
   204  	}
   205  	return resp, err
   206  }
   207  
   208  // Introspect returns basic status about this outbound.
   209  func (o *ChannelOutbound) Introspect() introspection.OutboundStatus {
   210  	state := "Stopped"
   211  	if o.IsRunning() {
   212  		state = "Running"
   213  	}
   214  	return introspection.OutboundStatus{
   215  		Transport: "tchannel",
   216  		Endpoint:  o.addr,
   217  		State:     state,
   218  	}
   219  }