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  }