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 }