go.uber.org/yarpc@v1.72.1/transport/tchannel/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 "bytes" 25 "context" 26 "io" 27 "strconv" 28 29 "github.com/uber/tchannel-go" 30 "go.uber.org/yarpc/api/peer" 31 "go.uber.org/yarpc/api/transport" 32 "go.uber.org/yarpc/api/x/introspection" 33 "go.uber.org/yarpc/internal/bufferpool" 34 "go.uber.org/yarpc/internal/iopool" 35 intyarpcerrors "go.uber.org/yarpc/internal/yarpcerrors" 36 peerchooser "go.uber.org/yarpc/peer" 37 "go.uber.org/yarpc/peer/hostport" 38 "go.uber.org/yarpc/pkg/errors" 39 "go.uber.org/yarpc/pkg/lifecycle" 40 "go.uber.org/yarpc/yarpcerrors" 41 ) 42 43 var ( 44 errDoNotUseContextWithHeaders = yarpcerrors.Newf(yarpcerrors.CodeInvalidArgument, "tchannel.ContextWithHeaders is not compatible with YARPC, use yarpc.CallOption instead") 45 46 _ transport.UnaryOutbound = (*Outbound)(nil) 47 _ introspection.IntrospectableOutbound = (*Outbound)(nil) 48 ) 49 50 // Outbound sends YARPC requests over TChannel. 51 // It may be constructed using the NewOutbound or NewSingleOutbound methods on 52 // the TChannel Transport. 53 type Outbound struct { 54 transport *Transport 55 chooser peer.Chooser 56 once *lifecycle.Once 57 reuseBuffer bool 58 } 59 60 // OutboundOption customizes the behavior of a TChannel Outbound. 61 type OutboundOption func(o *Outbound) 62 63 // WithReuseBuffer configures the Outbound to 64 // use a buffer pool to read the response bytes. 65 func WithReuseBuffer(enable bool) OutboundOption { 66 return func(o *Outbound) { 67 o.reuseBuffer = enable 68 } 69 } 70 71 // NewOutbound builds a new TChannel outbound that selects a peer for each 72 // request using the given peer chooser. 73 func (t *Transport) NewOutbound(chooser peer.Chooser, opts ...OutboundOption) *Outbound { 74 o := &Outbound{ 75 once: lifecycle.NewOnce(), 76 transport: t, 77 chooser: chooser, 78 } 79 for _, opt := range opts { 80 opt(o) 81 } 82 return o 83 } 84 85 // NewSingleOutbound builds a new TChannel outbound always using the peer with 86 // the given address. 87 func (t *Transport) NewSingleOutbound(addr string, opts ...OutboundOption) *Outbound { 88 chooser := peerchooser.NewSingle(hostport.PeerIdentifier(addr), t) 89 return t.NewOutbound(chooser, opts...) 90 } 91 92 // TransportName is the transport name that will be set on `transport.Request` struct. 93 func (o *Outbound) TransportName() string { 94 return TransportName 95 } 96 97 // Chooser returns the outbound's peer chooser. 98 func (o *Outbound) Chooser() peer.Chooser { 99 return o.chooser 100 } 101 102 // Call sends an RPC over this TChannel outbound. 103 func (o *Outbound) Call(ctx context.Context, req *transport.Request) (*transport.Response, error) { 104 if req == nil { 105 return nil, yarpcerrors.InvalidArgumentErrorf("request for tchannel outbound was nil") 106 } 107 if err := o.once.WaitUntilRunning(ctx); err != nil { 108 return nil, intyarpcerrors.AnnotateWithInfo(yarpcerrors.FromError(err), "error waiting for tchannel outbound to start for service: %s", req.Service) 109 } 110 if _, ok := ctx.(tchannel.ContextWithHeaders); ok { 111 return nil, errDoNotUseContextWithHeaders 112 } 113 p, onFinish, err := o.getPeerForRequest(ctx, req) 114 if err != nil { 115 return nil, toYARPCError(req, err) 116 } 117 res, err := p.Call(ctx, req, o.reuseBuffer) 118 onFinish(err) 119 return res, toYARPCError(req, err) 120 } 121 122 // Call sends an RPC to this specific peer. 123 func (p *tchannelPeer) Call(ctx context.Context, req *transport.Request, reuseBuffer bool) (*transport.Response, error) { 124 return callWithPeer(ctx, req, p.getPeer(), p.transport.headerCase, reuseBuffer) 125 } 126 127 // callWithPeer sends a request with the chosen peer. 128 func callWithPeer(ctx context.Context, req *transport.Request, peer *tchannel.Peer, headerCase headerCase, reuseBuffer bool) (*transport.Response, error) { 129 // NB(abg): Under the current API, the local service's name is required 130 // twice: once when constructing the TChannel and then again when 131 // constructing the RPC. 132 var call *tchannel.OutboundCall 133 var err error 134 135 format := tchannel.Format(req.Encoding) 136 callOptions := tchannel.CallOptions{ 137 Format: format, 138 CallerName: req.Caller, 139 ShardKey: req.ShardKey, 140 RoutingKey: req.RoutingKey, 141 RoutingDelegate: req.RoutingDelegate, 142 } 143 144 // If the hostport is given, we use the BeginCall on the channel 145 // instead of the subchannel. 146 call, err = peer.BeginCall( 147 // TODO(abg): Set TimeoutPerAttempt in the context's retry options if 148 // TTL is set. 149 // (kris): Consider instead moving TimeoutPerAttempt to an outer 150 // layer, just clamp the context on outbound call. 151 ctx, 152 req.Service, 153 req.Procedure, 154 &callOptions, 155 ) 156 157 if err != nil { 158 return nil, err 159 } 160 reqHeaders := headerMap(req.Headers, headerCase) 161 162 // for tchannel, callerProcedure is added to application headers. 163 reqHeaders = requestCallerProcedureToHeader(req, reqHeaders) 164 165 // baggage headers are transport implementation details that are stripped out (and stored in the context). Users don't interact with it 166 tracingBaggage := tchannel.InjectOutboundSpan(call.Response(), nil) 167 if err := writeHeaders(format, reqHeaders, tracingBaggage, call.Arg2Writer); err != nil { 168 // TODO(abg): This will wrap IO errors while writing headers as encode 169 // errors. We should fix that. 170 return nil, errors.RequestHeadersEncodeError(req, err) 171 } 172 173 if err := writeBody(req.Body, call); err != nil { 174 return nil, err 175 } 176 177 res := call.Response() 178 headers, err := readHeaders(format, res.Arg2Reader) 179 if err != nil { 180 if err, ok := err.(tchannel.SystemError); ok { 181 return nil, fromSystemError(err) 182 } 183 // TODO(abg): This will wrap IO errors while reading headers as decode 184 // errors. We should fix that. 185 return nil, errors.ResponseHeadersDecodeError(req, err) 186 } 187 188 resBody, err := res.Arg3Reader() 189 if err != nil { 190 if err, ok := err.(tchannel.SystemError); ok { 191 return nil, fromSystemError(err) 192 } 193 return nil, err 194 } 195 196 body, bodySize, err := getResponseBody(resBody, reuseBuffer) 197 if err != nil { 198 return nil, err 199 } 200 201 if err = resBody.Close(); err != nil { 202 return nil, err 203 } 204 205 respService, _ := headers.Get(ServiceHeaderKey) // validateServiceName handles empty strings 206 if err := validateServiceName(req.Service, respService); err != nil { 207 return nil, err 208 } 209 210 applicationErrorName, _ := headers.Get(ApplicationErrorNameHeaderKey) 211 applicationErrorCode := getApplicationErrorCodeFromHeaders(headers) 212 applicationErrorDetails, _ := headers.Get(ApplicationErrorDetailsHeaderKey) 213 214 err = getResponseError(headers) 215 deleteReservedHeaders(headers) 216 217 resp := &transport.Response{ 218 Headers: headers, 219 Body: body, 220 BodySize: bodySize, 221 ApplicationError: res.ApplicationError(), 222 ApplicationErrorMeta: &transport.ApplicationErrorMeta{ 223 Details: applicationErrorDetails, 224 Name: applicationErrorName, 225 Code: applicationErrorCode, 226 }, 227 } 228 return resp, err 229 } 230 231 func getResponseBody(resBody tchannel.ArgReader, reuseBuffer bool) (body io.ReadCloser, bodySize int, err error) { 232 if reuseBuffer { 233 buffer := bufferpool.NewAutoReleaseBuffer() 234 if _, err = buffer.ReadFrom(resBody); err != nil { 235 return nil, 0, err 236 } 237 body = readerCloser{ 238 Reader: bytes.NewReader(buffer.Bytes()), 239 Closer: buffer, 240 } 241 return body, buffer.Len(), nil 242 } 243 buffer := bytes.NewBuffer(make([]byte, 0, _defaultBufferSize)) 244 if _, err = buffer.ReadFrom(resBody); err != nil { 245 return nil, 0, err 246 } 247 248 body = io.NopCloser(bytes.NewReader(buffer.Bytes())) 249 return body, buffer.Len(), nil 250 } 251 252 func writeBody(body io.Reader, call *tchannel.OutboundCall) error { 253 w, err := call.Arg3Writer() 254 if err != nil { 255 return err 256 } 257 258 if _, err := iopool.Copy(w, body); err != nil { 259 return err 260 } 261 262 return w.Close() 263 } 264 265 func getResponseError(headers transport.Headers) error { 266 errorCodeString, ok := headers.Get(ErrorCodeHeaderKey) 267 if !ok { 268 return nil 269 } 270 var errorCode yarpcerrors.Code 271 if err := errorCode.UnmarshalText([]byte(errorCodeString)); err != nil { 272 return err 273 } 274 if errorCode == yarpcerrors.CodeOK { 275 return yarpcerrors.Newf(yarpcerrors.CodeInternal, "got CodeOK from error header") 276 } 277 errorName, _ := headers.Get(ErrorNameHeaderKey) 278 errorMessage, _ := headers.Get(ErrorMessageHeaderKey) 279 return intyarpcerrors.NewWithNamef(errorCode, errorName, errorMessage) 280 } 281 282 func getApplicationErrorCodeFromHeaders(headers transport.Headers) *yarpcerrors.Code { 283 errorCodeHeader, found := headers.Get(ApplicationErrorCodeHeaderKey) 284 if !found { 285 return nil 286 } 287 288 errorCode, err := strconv.Atoi(errorCodeHeader) 289 if err != nil { 290 return nil 291 } 292 293 yarpcCode := yarpcerrors.Code(errorCode) 294 return &yarpcCode 295 } 296 297 func (o *Outbound) getPeerForRequest(ctx context.Context, treq *transport.Request) (*tchannelPeer, func(error), error) { 298 p, onFinish, err := o.chooser.Choose(ctx, treq) 299 if err != nil { 300 return nil, nil, err 301 } 302 303 tp, ok := p.(*tchannelPeer) 304 if !ok { 305 return nil, nil, peer.ErrInvalidPeerConversion{ 306 Peer: p, 307 ExpectedType: "*tchannelPeer", 308 } 309 } 310 311 return tp, onFinish, nil 312 } 313 314 // Transports returns the underlying TChannel Transport for this outbound. 315 func (o *Outbound) Transports() []transport.Transport { 316 return []transport.Transport{o.transport} 317 } 318 319 // Start starts the TChannel outbound. 320 func (o *Outbound) Start() error { 321 return o.once.Start(o.chooser.Start) 322 } 323 324 // Stop stops the TChannel outbound. 325 func (o *Outbound) Stop() error { 326 return o.once.Stop(o.chooser.Stop) 327 } 328 329 // IsRunning returns whether the ChannelOutbound is running. 330 func (o *Outbound) IsRunning() bool { 331 return o.once.IsRunning() 332 } 333 334 // Introspect returns basic status about this outbound. 335 func (o *Outbound) Introspect() introspection.OutboundStatus { 336 state := "Stopped" 337 if o.IsRunning() { 338 state = "Running" 339 } 340 var chooser introspection.ChooserStatus 341 if i, ok := o.chooser.(introspection.IntrospectableChooser); ok { 342 chooser = i.Introspect() 343 } else { 344 chooser = introspection.ChooserStatus{ 345 Name: "Introspection not available", 346 } 347 } 348 return introspection.OutboundStatus{ 349 Transport: "tchannel", 350 State: state, 351 Chooser: chooser, 352 } 353 } 354 355 type readerCloser struct { 356 *bytes.Reader 357 io.Closer 358 }