github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/colflow/colrpc/outbox.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package colrpc 12 13 import ( 14 "bytes" 15 "context" 16 "io" 17 "sync/atomic" 18 19 "github.com/cockroachdb/cockroach/pkg/col/coldata" 20 "github.com/cockroachdb/cockroach/pkg/col/colserde" 21 "github.com/cockroachdb/cockroach/pkg/roachpb" 22 "github.com/cockroachdb/cockroach/pkg/rpc" 23 "github.com/cockroachdb/cockroach/pkg/sql/colexec" 24 "github.com/cockroachdb/cockroach/pkg/sql/colexecbase" 25 "github.com/cockroachdb/cockroach/pkg/sql/colexecbase/colexecerror" 26 "github.com/cockroachdb/cockroach/pkg/sql/colmem" 27 "github.com/cockroachdb/cockroach/pkg/sql/execinfrapb" 28 "github.com/cockroachdb/cockroach/pkg/sql/types" 29 "github.com/cockroachdb/cockroach/pkg/util/log" 30 "github.com/cockroachdb/errors" 31 "github.com/cockroachdb/logtags" 32 "google.golang.org/grpc" 33 ) 34 35 // flowStreamClient is a utility interface used to mock out the RPC layer. 36 type flowStreamClient interface { 37 Send(*execinfrapb.ProducerMessage) error 38 Recv() (*execinfrapb.ConsumerSignal, error) 39 CloseSend() error 40 } 41 42 // Dialer is used for dialing based on node IDs. It extracts out the single 43 // method that Outbox.Run needs from nodedialer.Dialer so that we can mock it 44 // in tests outside of this package. 45 type Dialer interface { 46 Dial(context.Context, roachpb.NodeID, rpc.ConnectionClass) (*grpc.ClientConn, error) 47 } 48 49 // Outbox is used to push data from local flows to a remote endpoint. Run may 50 // be called with the necessary information to establish a connection to a 51 // given remote endpoint. 52 type Outbox struct { 53 colexec.OneInputNode 54 55 typs []*types.T 56 // batch is the last batch received from the input. 57 batch coldata.Batch 58 59 converter *colserde.ArrowBatchConverter 60 serializer *colserde.RecordBatchSerializer 61 62 // draining is an atomic that represents whether the Outbox is draining. 63 draining uint32 64 metadataSources []execinfrapb.MetadataSource 65 // closers is a slice of Closers that need to be Closed on termination. 66 closers []colexec.IdempotentCloser 67 68 scratch struct { 69 buf *bytes.Buffer 70 msg *execinfrapb.ProducerMessage 71 } 72 73 // A copy of Run's caller ctx, with no StreamID tag. 74 // Used to pass a clean context to the input.Next. 75 runnerCtx context.Context 76 } 77 78 // NewOutbox creates a new Outbox. 79 func NewOutbox( 80 allocator *colmem.Allocator, 81 input colexecbase.Operator, 82 typs []*types.T, 83 metadataSources []execinfrapb.MetadataSource, 84 toClose []colexec.IdempotentCloser, 85 ) (*Outbox, error) { 86 c, err := colserde.NewArrowBatchConverter(typs) 87 if err != nil { 88 return nil, err 89 } 90 s, err := colserde.NewRecordBatchSerializer(typs) 91 if err != nil { 92 return nil, err 93 } 94 o := &Outbox{ 95 // Add a deselector as selection vectors are not serialized (nor should they 96 // be). 97 OneInputNode: colexec.NewOneInputNode(colexec.NewDeselectorOp(allocator, input, typs)), 98 typs: typs, 99 converter: c, 100 serializer: s, 101 metadataSources: metadataSources, 102 closers: toClose, 103 } 104 o.scratch.buf = &bytes.Buffer{} 105 o.scratch.msg = &execinfrapb.ProducerMessage{} 106 return o, nil 107 } 108 109 func (o *Outbox) close(ctx context.Context) { 110 for _, closer := range o.closers { 111 if err := closer.IdempotentClose(ctx); err != nil { 112 if log.V(1) { 113 log.Infof(ctx, "error closing Closer: %v", err) 114 } 115 } 116 } 117 } 118 119 // Run starts an outbox by connecting to the provided node and pushing 120 // coldata.Batches over the stream after sending a header with the provided flow 121 // and stream ID. Note that an extra goroutine is spawned so that Recv may be 122 // called concurrently wrt the Send goroutine to listen for drain signals. 123 // If an io.EOF is received while sending, the outbox will call cancelFn to 124 // indicate an unexpected termination of the stream. 125 // If an error is encountered that cannot be sent over the stream, the error 126 // will be logged but not returned. 127 // There are several ways the bidirectional FlowStream RPC may terminate. 128 // 1) Execution is finished. In this case, the upstream operator signals 129 // termination by returning a zero-length batch. The Outbox will drain its 130 // metadata sources, send the metadata, and then call CloseSend on the 131 // stream. The Outbox will wait until its Recv goroutine receives a non-nil 132 // error to not leak resources. 133 // 2) A cancellation happened. This can come from the provided context or the 134 // remote reader. Refer to tests for expected behavior. 135 // 3) A drain signal was received from the server (consumer). In this case, the 136 // Outbox goes through the same steps as 1). 137 func (o *Outbox) Run( 138 ctx context.Context, 139 dialer Dialer, 140 nodeID roachpb.NodeID, 141 flowID execinfrapb.FlowID, 142 streamID execinfrapb.StreamID, 143 cancelFn context.CancelFunc, 144 ) { 145 o.runnerCtx = ctx 146 ctx = logtags.AddTag(ctx, "streamID", streamID) 147 log.VEventf(ctx, 2, "Outbox Dialing %s", nodeID) 148 149 var stream execinfrapb.DistSQL_FlowStreamClient 150 if err := func() error { 151 conn, err := dialer.Dial(ctx, nodeID, rpc.DefaultClass) 152 if err != nil { 153 log.Warningf( 154 ctx, 155 "Outbox Dial connection error, distributed query will fail: %+v", 156 err, 157 ) 158 return err 159 } 160 161 client := execinfrapb.NewDistSQLClient(conn) 162 stream, err = client.FlowStream(ctx) 163 if err != nil { 164 log.Warningf( 165 ctx, 166 "Outbox FlowStream connection error, distributed query will fail: %+v", 167 err, 168 ) 169 return err 170 } 171 172 log.VEvent(ctx, 2, "Outbox sending header") 173 // Send header message to establish the remote server (consumer). 174 if err := stream.Send( 175 &execinfrapb.ProducerMessage{Header: &execinfrapb.ProducerHeader{FlowID: flowID, StreamID: streamID}}, 176 ); err != nil { 177 log.Warningf( 178 ctx, 179 "Outbox Send header error, distributed query will fail: %+v", 180 err, 181 ) 182 return err 183 } 184 return nil 185 }(); err != nil { 186 // error during stream set up. 187 o.close(ctx) 188 return 189 } 190 191 log.VEvent(ctx, 2, "Outbox starting normal operation") 192 o.runWithStream(ctx, stream, cancelFn) 193 log.VEvent(ctx, 2, "Outbox exiting") 194 } 195 196 // handleStreamErr is a utility method used to handle an error when calling 197 // a method on a flowStreamClient. If err is an io.EOF, cancelFn is called. The 198 // given error is logged with the associated opName. 199 func (o *Outbox) handleStreamErr( 200 ctx context.Context, opName string, err error, cancelFn context.CancelFunc, 201 ) { 202 if err == io.EOF { 203 if log.V(1) { 204 log.Infof(ctx, "Outbox calling cancelFn after %s EOF", opName) 205 } 206 cancelFn() 207 } else { 208 if log.V(1) { 209 log.Warningf(ctx, "Outbox %s connection error: %+v", opName, err) 210 } 211 } 212 } 213 214 func (o *Outbox) moveToDraining(ctx context.Context) { 215 if atomic.CompareAndSwapUint32(&o.draining, 0, 1) { 216 log.VEvent(ctx, 2, "Outbox moved to draining") 217 } 218 } 219 220 // sendBatches reads from the Outbox's input in a loop and sends the 221 // coldata.Batches over the stream. A boolean is returned, indicating whether 222 // execution completed gracefully (either received a zero-length batch or a 223 // drain signal) as well as an error which is non-nil if an error was 224 // encountered AND the error should be sent over the stream as metadata. The for 225 // loop continues iterating until one of the following conditions becomes true: 226 // 1) A zero-length batch is received from the input. This indicates graceful 227 // termination. true, nil is returned. 228 // 2) Outbox.draining is observed to be true. This is also considered graceful 229 // termination. true, nil is returned. 230 // 3) An error unrelated to the stream occurs (e.g. while deserializing a 231 // coldata.Batch). false, err is returned. This err should be sent over the 232 // stream as metadata. 233 // 4) An error related to the stream occurs. In this case, the error is logged 234 // but not returned, as there is no way to propagate this error anywhere 235 // meaningful. false, nil is returned. NOTE: io.EOF is a special case. This 236 // indicates non-graceful termination initiated by the remote Inbox. cancelFn 237 // will be called in this case. 238 func (o *Outbox) sendBatches( 239 ctx context.Context, stream flowStreamClient, cancelFn context.CancelFunc, 240 ) (terminatedGracefully bool, _ error) { 241 nextBatch := func() { 242 if o.runnerCtx == nil { 243 o.runnerCtx = ctx 244 } 245 o.batch = o.Input().Next(o.runnerCtx) 246 } 247 serializeBatch := func() { 248 o.scratch.buf.Reset() 249 d, err := o.converter.BatchToArrow(o.batch) 250 if err != nil { 251 colexecerror.InternalError(errors.Wrap(err, "Outbox BatchToArrow data serialization error")) 252 } 253 if _, _, err := o.serializer.Serialize(o.scratch.buf, d); err != nil { 254 colexecerror.InternalError(errors.Wrap(err, "Outbox Serialize data error")) 255 } 256 } 257 for { 258 if atomic.LoadUint32(&o.draining) == 1 { 259 return true, nil 260 } 261 262 if err := colexecerror.CatchVectorizedRuntimeError(nextBatch); err != nil { 263 if log.V(1) { 264 log.Warningf(ctx, "Outbox Next error: %+v", err) 265 } 266 return false, err 267 } 268 if o.batch.Length() == 0 { 269 return true, nil 270 } 271 272 if err := colexecerror.CatchVectorizedRuntimeError(serializeBatch); err != nil { 273 log.Errorf(ctx, "%+v", err) 274 return false, err 275 } 276 o.scratch.msg.Data.RawBytes = o.scratch.buf.Bytes() 277 278 // o.scratch.msg can be reused as soon as Send returns since it returns as 279 // soon as the message is written to the control buffer. The message is 280 // marshaled (bytes are copied) before writing. 281 if err := stream.Send(o.scratch.msg); err != nil { 282 o.handleStreamErr(ctx, "Send (batches)", err, cancelFn) 283 return false, nil 284 } 285 } 286 } 287 288 // sendMetadata drains the Outbox.metadataSources and sends the metadata over 289 // the given stream, returning the Send error, if any. sendMetadata also sends 290 // errToSend as metadata if non-nil. 291 func (o *Outbox) sendMetadata(ctx context.Context, stream flowStreamClient, errToSend error) error { 292 msg := &execinfrapb.ProducerMessage{} 293 if errToSend != nil { 294 msg.Data.Metadata = append( 295 msg.Data.Metadata, execinfrapb.LocalMetaToRemoteProducerMeta(ctx, execinfrapb.ProducerMetadata{Err: errToSend}), 296 ) 297 } 298 for _, src := range o.metadataSources { 299 for _, meta := range src.DrainMeta(ctx) { 300 msg.Data.Metadata = append(msg.Data.Metadata, execinfrapb.LocalMetaToRemoteProducerMeta(ctx, meta)) 301 } 302 } 303 if len(msg.Data.Metadata) == 0 { 304 return nil 305 } 306 return stream.Send(msg) 307 } 308 309 // runWithStream should be called after sending the ProducerHeader on the 310 // stream. It implements the behavior described in Run. 311 func (o *Outbox) runWithStream( 312 ctx context.Context, stream flowStreamClient, cancelFn context.CancelFunc, 313 ) { 314 o.Input().Init() 315 316 waitCh := make(chan struct{}) 317 go func() { 318 for { 319 msg, err := stream.Recv() 320 if err != nil { 321 if err != io.EOF { 322 if log.V(1) { 323 log.Warningf(ctx, "Outbox Recv connection error: %+v", err) 324 } 325 } 326 break 327 } 328 switch { 329 case msg.Handshake != nil: 330 log.VEventf(ctx, 2, "Outbox received handshake: %v", msg.Handshake) 331 case msg.DrainRequest != nil: 332 o.moveToDraining(ctx) 333 } 334 } 335 close(waitCh) 336 }() 337 338 terminatedGracefully, errToSend := o.sendBatches(ctx, stream, cancelFn) 339 if terminatedGracefully || errToSend != nil { 340 o.moveToDraining(ctx) 341 if err := o.sendMetadata(ctx, stream, errToSend); err != nil { 342 o.handleStreamErr(ctx, "Send (metadata)", err, cancelFn) 343 } else { 344 // Close the stream. Note that if this block isn't reached, the stream 345 // is unusable. 346 // The receiver goroutine will read from the stream until io.EOF is 347 // returned. 348 if err := stream.CloseSend(); err != nil { 349 o.handleStreamErr(ctx, "CloseSend", err, cancelFn) 350 } 351 } 352 } 353 354 o.close(ctx) 355 <-waitCh 356 }