github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/topic/topicwriterinternal/writer_single_stream.go (about)

     1  package topicwriterinternal
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"reflect"
     8  	"sync/atomic"
     9  
    10  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/background"
    11  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicwriter"
    14  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    15  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    16  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    17  )
    18  
    19  var errSingleStreamWriterDoubleClose = xerrors.Wrap(errors.New("ydb: single stream writer impl double closed"))
    20  
    21  type SingleStreamWriterConfig struct {
    22  	WritersCommonConfig
    23  
    24  	stream                RawTopicWriterStream
    25  	queue                 *messageQueue
    26  	encodersMap           *EncoderMap
    27  	getLastSeqNum         bool
    28  	reconnectorInstanceID string
    29  }
    30  
    31  func newSingleStreamWriterConfig(
    32  	common WritersCommonConfig, //nolint:gocritic
    33  	stream RawTopicWriterStream,
    34  	queue *messageQueue,
    35  	encodersMap *EncoderMap,
    36  	getLastSeqNum bool,
    37  	reconnectorID string,
    38  ) SingleStreamWriterConfig {
    39  	return SingleStreamWriterConfig{
    40  		WritersCommonConfig:   common,
    41  		stream:                stream,
    42  		queue:                 queue,
    43  		encodersMap:           encodersMap,
    44  		getLastSeqNum:         getLastSeqNum,
    45  		reconnectorInstanceID: reconnectorID,
    46  	}
    47  }
    48  
    49  type SingleStreamWriter struct {
    50  	cfg                 SingleStreamWriterConfig
    51  	Encoder             EncoderSelector
    52  	background          background.Worker
    53  	CodecsFromServer    rawtopiccommon.SupportedCodecs
    54  	allowedCodecs       rawtopiccommon.SupportedCodecs
    55  	SessionID           string
    56  	closeReason         error
    57  	ReceivedLastSeqNum  int64
    58  	PartitionID         int64
    59  	closeCompleted      empty.Chan
    60  	closed              atomic.Bool
    61  	LastSeqNumRequested bool
    62  }
    63  
    64  func NewSingleStreamWriter(
    65  	ctxForPProfLabelsOnly context.Context,
    66  	cfg SingleStreamWriterConfig, //nolint:gocritic
    67  ) (*SingleStreamWriter, error) {
    68  	res := newSingleStreamWriterStopped(ctxForPProfLabelsOnly, cfg)
    69  
    70  	if err := res.initStream(); err != nil {
    71  		_ = res.close(context.Background(), err)
    72  
    73  		return nil, err
    74  	}
    75  
    76  	res.start()
    77  
    78  	return res, nil
    79  }
    80  
    81  func newSingleStreamWriterStopped(
    82  	ctxForPProfLabelsOnly context.Context,
    83  	cfg SingleStreamWriterConfig, //nolint:gocritic
    84  ) *SingleStreamWriter {
    85  	return &SingleStreamWriter{
    86  		cfg: cfg,
    87  		background: *background.NewWorker(
    88  			xcontext.ValueOnly(ctxForPProfLabelsOnly),
    89  			"ydb-topic-stream-writer-background",
    90  		),
    91  		closeCompleted: make(empty.Chan),
    92  	}
    93  }
    94  
    95  func (w *SingleStreamWriter) close(ctx context.Context, reason error) error {
    96  	if !w.closed.CompareAndSwap(false, true) {
    97  		return xerrors.WithStackTrace(errSingleStreamWriterDoubleClose)
    98  	}
    99  
   100  	defer close(w.closeCompleted)
   101  	w.closeReason = reason
   102  
   103  	resErr := w.cfg.stream.CloseSend()
   104  	bgWaitErr := w.background.Close(ctx, reason)
   105  	if resErr == nil {
   106  		resErr = bgWaitErr
   107  	}
   108  
   109  	return resErr
   110  }
   111  
   112  func (w *SingleStreamWriter) WaitClose(ctx context.Context) error {
   113  	select {
   114  	case <-ctx.Done():
   115  		return ctx.Err()
   116  	case <-w.closeCompleted:
   117  		return w.closeReason
   118  	}
   119  }
   120  
   121  func (w *SingleStreamWriter) start() {
   122  	w.background.Start("topic writer update token", w.updateTokenLoop)
   123  	w.background.Start("topic writer send messages", w.sendMessagesFromQueueToStreamLoop)
   124  	w.background.Start("topic writer receive messages", w.receiveMessagesLoop)
   125  }
   126  
   127  func (w *SingleStreamWriter) initStream() (err error) {
   128  	traceOnDone := trace.TopicOnWriterInitStream(w.cfg.Tracer, w.cfg.reconnectorInstanceID, w.cfg.topic, w.cfg.producerID)
   129  	defer traceOnDone(w.SessionID, err)
   130  
   131  	req := w.createInitRequest()
   132  	if err = w.cfg.stream.Send(&req); err != nil {
   133  		return err
   134  	}
   135  	recvMessage, err := w.cfg.stream.Recv()
   136  	if err != nil {
   137  		return err
   138  	}
   139  	result, ok := recvMessage.(*rawtopicwriter.InitResult)
   140  	if !ok {
   141  		return xerrors.WithStackTrace(
   142  			fmt.Errorf("ydb: failed init response message type: %v", reflect.TypeOf(recvMessage)),
   143  		)
   144  	}
   145  
   146  	w.allowedCodecs = calculateAllowedCodecs(w.cfg.forceCodec, w.cfg.encodersMap, result.SupportedCodecs)
   147  	if len(w.allowedCodecs) == 0 {
   148  		return xerrors.WithStackTrace(errNoAllowedCodecs)
   149  	}
   150  
   151  	w.Encoder = NewEncoderSelector(
   152  		w.cfg.encodersMap,
   153  		w.allowedCodecs,
   154  		w.cfg.compressorCount,
   155  		w.cfg.Tracer,
   156  		w.cfg.reconnectorInstanceID,
   157  		w.SessionID,
   158  	)
   159  
   160  	w.SessionID = result.SessionID
   161  	w.LastSeqNumRequested = req.GetLastSeqNo
   162  	w.ReceivedLastSeqNum = result.LastSeqNo
   163  	w.PartitionID = result.PartitionID
   164  	w.CodecsFromServer = result.SupportedCodecs
   165  
   166  	return nil
   167  }
   168  
   169  func (w *SingleStreamWriter) createInitRequest() rawtopicwriter.InitRequest {
   170  	return rawtopicwriter.InitRequest{
   171  		Path:             w.cfg.topic,
   172  		ProducerID:       w.cfg.producerID,
   173  		WriteSessionMeta: w.cfg.writerMeta,
   174  		Partitioning:     w.cfg.defaultPartitioning,
   175  		GetLastSeqNo:     w.cfg.getLastSeqNum,
   176  	}
   177  }
   178  
   179  func (w *SingleStreamWriter) receiveMessagesLoop(ctx context.Context) {
   180  	for {
   181  		if ctx.Err() != nil {
   182  			return
   183  		}
   184  
   185  		mess, err := w.cfg.stream.Recv()
   186  		if err != nil {
   187  			err = xerrors.WithStackTrace(fmt.Errorf("ydb: failed to receive message from write stream: %w", err))
   188  			_ = w.close(ctx, err)
   189  
   190  			return
   191  		}
   192  
   193  		switch m := mess.(type) {
   194  		case *rawtopicwriter.WriteResult:
   195  			if err = w.cfg.queue.AcksReceived(m.Acks); err != nil && !errors.Is(err, errCloseClosedMessageQueue) {
   196  				reason := xerrors.WithStackTrace(err)
   197  				closeCtx, closeCtxCancel := xcontext.WithCancel(ctx)
   198  				closeCtxCancel()
   199  				_ = w.close(closeCtx, reason)
   200  
   201  				return
   202  			}
   203  		case *rawtopicwriter.UpdateTokenResponse:
   204  			// pass
   205  		default:
   206  			trace.TopicOnWriterReadUnknownGrpcMessage(
   207  				w.cfg.Tracer,
   208  				w.cfg.reconnectorInstanceID,
   209  				w.SessionID,
   210  				xerrors.WithStackTrace(xerrors.Wrap(fmt.Errorf(
   211  					"ydb: unexpected message type in stream reader: %v",
   212  					reflect.TypeOf(m),
   213  				))),
   214  			)
   215  		}
   216  	}
   217  }
   218  
   219  func (w *SingleStreamWriter) sendMessagesFromQueueToStreamLoop(ctx context.Context) {
   220  	for {
   221  		messages, err := w.cfg.queue.GetMessagesForSend(ctx)
   222  		if err != nil {
   223  			_ = w.close(ctx, err)
   224  
   225  			return
   226  		}
   227  
   228  		targetCodec, err := w.Encoder.CompressMessages(messages)
   229  		if err != nil {
   230  			_ = w.close(ctx, err)
   231  
   232  			return
   233  		}
   234  
   235  		onSentComplete := trace.TopicOnWriterSendMessages(
   236  			w.cfg.Tracer,
   237  			w.cfg.reconnectorInstanceID,
   238  			w.SessionID,
   239  			targetCodec.ToInt32(),
   240  			messages[0].SeqNo,
   241  			len(messages),
   242  		)
   243  		err = sendMessagesToStream(w.cfg.stream, targetCodec, messages)
   244  		onSentComplete(err)
   245  		if err != nil {
   246  			err = xerrors.WithStackTrace(fmt.Errorf("ydb: error send message to topic stream: %w", err))
   247  			_ = w.close(ctx, err)
   248  
   249  			return
   250  		}
   251  	}
   252  }
   253  
   254  func (w *SingleStreamWriter) updateTokenLoop(ctx context.Context) {
   255  	if ctx.Err() != nil {
   256  		return
   257  	}
   258  
   259  	ticker := w.cfg.clock.NewTicker(w.cfg.credUpdateInterval)
   260  	defer ticker.Stop()
   261  
   262  	ctxDone := ctx.Done()
   263  	tickerChan := ticker.Chan()
   264  	for {
   265  		select {
   266  		case <-ctxDone:
   267  			return
   268  		case <-tickerChan:
   269  			_ = w.sendUpdateToken(ctx)
   270  		}
   271  	}
   272  }
   273  
   274  func (w *SingleStreamWriter) sendUpdateToken(ctx context.Context) (err error) {
   275  	token, err := w.cfg.cred.Token(ctx)
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	stream := w.cfg.stream
   281  	if stream == nil {
   282  		// not connected yet
   283  		return nil
   284  	}
   285  
   286  	req := &rawtopicwriter.UpdateTokenRequest{}
   287  	req.Token = token
   288  
   289  	return stream.Send(req)
   290  }
   291  
   292  //go:generate mockgen -destination raw_topic_writer_stream_mock_test.go --typed -package topicwriterinternal -write_package_comment=false github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicwriterinternal RawTopicWriterStream
   293  
   294  type RawTopicWriterStream interface {
   295  	Recv() (rawtopicwriter.ServerMessage, error)
   296  	Send(mess rawtopicwriter.ClientMessage) error
   297  	CloseSend() error
   298  }