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

     1  package topicwriterinternal
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"math/big"
    10  	"runtime"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/jonboulle/clockwork"
    16  	"golang.org/x/sync/semaphore"
    17  
    18  	"github.com/ydb-platform/ydb-go-sdk/v3/credentials"
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/background"
    20  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/config"
    21  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicwriter"
    24  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/topic"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/value"
    26  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    27  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    28  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    29  	"github.com/ydb-platform/ydb-go-sdk/v3/topic/topictypes"
    30  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    31  )
    32  
    33  var (
    34  	errConnTimeout                                 = xerrors.Wrap(errors.New("ydb: connection timeout"))
    35  	errStopWriterReconnector                       = xerrors.Wrap(errors.New("ydb: stop writer reconnector"))
    36  	errNonZeroSeqNo                                = xerrors.Wrap(errors.New("ydb: non zero seqno for auto set seqno mode"))                         //nolint:lll
    37  	errNonZeroCreatedAt                            = xerrors.Wrap(errors.New("ydb: non zero Message.CreatedAt and set auto fill created at option")) //nolint:lll
    38  	errNoAllowedCodecs                             = xerrors.Wrap(errors.New("ydb: no allowed codecs for write to topic"))
    39  	errLargeMessage                                = xerrors.Wrap(errors.New("ydb: message uncompressed size more, then limit")) //nolint:lll
    40  	PublicErrQueueIsFull                           = xerrors.Wrap(errors.New("ydb: queue is full"))
    41  	PublicErrMessagesPutToInternalQueueBeforeError = xerrors.Wrap(errors.New("ydb: the messages was put to internal buffer before the error happened. It mean about the messages can be delivered to the server"))                                                                                                           //nolint:lll
    42  	errDiffetentTransactions                       = xerrors.Wrap(errors.New("ydb: internal writer has messages from different trasactions. It is internal logic error, write issue please: https://github.com/ydb-platform/ydb-go-sdk/issues/new?assignees=&labels=bug&projects=&template=01_BUG_REPORT.md&title=bug%3A+")) //nolint:lll
    43  
    44  	// errProducerIDNotEqualMessageGroupID is temporary
    45  	// WithMessageGroupID is optional parameter because it allowed to be skipped by protocol.
    46  	// But right not YDB server doesn't implement it.
    47  	// It is fast check for return error at writer create context instead of stream initialization
    48  	// The error will remove in the future, when skip message group id will be allowed by server.
    49  	errProducerIDNotEqualMessageGroupID = xerrors.Wrap(errors.New("ydb: producer id not equal to message group id, use option WithMessageGroupID(producerID) for create writer")) //nolint:lll
    50  )
    51  
    52  type WriterReconnectorConfig struct {
    53  	WritersCommonConfig
    54  
    55  	MaxMessageSize               int
    56  	MaxQueueLen                  int
    57  	Common                       config.Common
    58  	AdditionalEncoders           map[rawtopiccommon.Codec]PublicCreateEncoderFunc
    59  	Connect                      ConnectFunc
    60  	WaitServerAck                bool
    61  	AutoSetSeqNo                 bool
    62  	AutoSetCreatedTime           bool
    63  	OnWriterInitResponseCallback PublicOnWriterInitResponseCallback
    64  	RetrySettings                topic.RetrySettings
    65  
    66  	connectTimeout time.Duration
    67  }
    68  
    69  func (cfg *WriterReconnectorConfig) validate() error {
    70  	if cfg.defaultPartitioning.Type == rawtopicwriter.PartitioningMessageGroupID &&
    71  		cfg.producerID != cfg.defaultPartitioning.MessageGroupID {
    72  		return xerrors.WithStackTrace(errProducerIDNotEqualMessageGroupID)
    73  	}
    74  
    75  	return nil
    76  }
    77  
    78  func NewWriterReconnectorConfig(options ...PublicWriterOption) WriterReconnectorConfig {
    79  	cfg := WriterReconnectorConfig{
    80  		WritersCommonConfig: WritersCommonConfig{
    81  			cred:               credentials.NewAnonymousCredentials(),
    82  			credUpdateInterval: time.Hour,
    83  			clock:              clockwork.NewRealClock(),
    84  			compressorCount:    runtime.NumCPU(),
    85  			Tracer:             &trace.Topic{},
    86  		},
    87  		AutoSetSeqNo:       true,
    88  		AutoSetCreatedTime: true,
    89  		MaxMessageSize:     50 * 1024 * 1024, //nolint:gomnd
    90  		MaxQueueLen:        1000,             //nolint:gomnd
    91  		RetrySettings: topic.RetrySettings{
    92  			StartTimeout: topic.DefaultStartTimeout,
    93  		},
    94  	}
    95  	if cfg.compressorCount == 0 {
    96  		cfg.compressorCount = 1
    97  	}
    98  
    99  	for _, f := range options {
   100  		f(&cfg)
   101  	}
   102  
   103  	if cfg.connectTimeout == 0 {
   104  		cfg.connectTimeout = cfg.Common.OperationTimeout()
   105  	}
   106  
   107  	if cfg.connectTimeout == 0 {
   108  		cfg.connectTimeout = value.InfiniteDuration
   109  	}
   110  
   111  	if cfg.producerID == "" {
   112  		WithProducerID(uuid.NewString())(&cfg)
   113  	}
   114  
   115  	return cfg
   116  }
   117  
   118  type WriterReconnector struct {
   119  	cfg                            WriterReconnectorConfig
   120  	queue                          messageQueue
   121  	background                     background.Worker
   122  	retrySettings                  topic.RetrySettings
   123  	writerInstanceID               string
   124  	semaphore                      *semaphore.Weighted
   125  	firstInitResponseProcessedChan empty.Chan
   126  	lastSeqNo                      int64
   127  	encodersMap                    *EncoderMap
   128  	initDoneCh                     empty.Chan
   129  	initInfo                       InitialInfo
   130  	m                              xsync.RWMutex
   131  	sessionID                      string
   132  	firstConnectionHandled         atomic.Bool
   133  	initDone                       bool
   134  }
   135  
   136  func NewWriterReconnector(
   137  	cfg WriterReconnectorConfig,
   138  ) (*WriterReconnector, error) {
   139  	if err := cfg.validate(); err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	res := newWriterReconnectorStopped(cfg)
   144  	res.start()
   145  
   146  	return res, nil
   147  }
   148  
   149  func newWriterReconnectorStopped(
   150  	cfg WriterReconnectorConfig, //nolint:gocritic
   151  ) *WriterReconnector {
   152  	writerInstanceID, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
   153  	res := &WriterReconnector{
   154  		cfg:                            cfg,
   155  		semaphore:                      semaphore.NewWeighted(int64(cfg.MaxQueueLen)),
   156  		queue:                          newMessageQueue(),
   157  		lastSeqNo:                      -1,
   158  		firstInitResponseProcessedChan: make(empty.Chan),
   159  		encodersMap:                    NewEncoderMap(),
   160  		writerInstanceID:               writerInstanceID.String(),
   161  		retrySettings:                  cfg.RetrySettings,
   162  	}
   163  
   164  	res.queue.OnAckReceived = res.onAckReceived
   165  
   166  	for codec, creator := range cfg.AdditionalEncoders {
   167  		res.encodersMap.AddEncoder(codec, creator)
   168  	}
   169  
   170  	res.sessionID = "not-connected-" + writerInstanceID.String()
   171  
   172  	res.initDoneCh = make(empty.Chan)
   173  
   174  	return res
   175  }
   176  
   177  func (w *WriterReconnector) fillFields(messages []messageWithDataContent) error {
   178  	var now time.Time
   179  
   180  	for i := range messages {
   181  		msg := &messages[i]
   182  
   183  		// SetSeqNo
   184  		if w.cfg.AutoSetSeqNo {
   185  			if msg.SeqNo != 0 {
   186  				return xerrors.WithStackTrace(errNonZeroSeqNo)
   187  			}
   188  			w.lastSeqNo++
   189  			msg.SeqNo = w.lastSeqNo
   190  		}
   191  
   192  		// Set created time
   193  		if w.cfg.AutoSetCreatedTime {
   194  			if msg.CreatedAt.IsZero() {
   195  				if now.IsZero() {
   196  					now = w.cfg.clock.Now()
   197  				}
   198  				msg.CreatedAt = now
   199  			} else {
   200  				return xerrors.WithStackTrace(errNonZeroCreatedAt)
   201  			}
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func (w *WriterReconnector) start() {
   209  	name := fmt.Sprintf("writer %q", w.cfg.topic)
   210  	w.background.Start(name+", sendloop", w.connectionLoop)
   211  }
   212  
   213  func (w *WriterReconnector) Write(ctx context.Context, messages []PublicMessage) (resErr error) {
   214  	if err := w.background.CloseReason(); err != nil {
   215  		return xerrors.WithStackTrace(fmt.Errorf("ydb: writer is closed: %w", err))
   216  	}
   217  	if ctx.Err() != nil {
   218  		return ctx.Err()
   219  	}
   220  	if len(messages) == 0 {
   221  		return nil
   222  	}
   223  
   224  	semaphoreWeight := int64(len(messages))
   225  	if semaphoreWeight > int64(w.cfg.MaxQueueLen) {
   226  		return xerrors.WithStackTrace(fmt.Errorf(
   227  			"ydb: add more messages, then max queue limit. max queue: %v, try to add: %v: %w",
   228  			w.cfg.MaxQueueLen,
   229  			semaphoreWeight,
   230  			PublicErrQueueIsFull,
   231  		))
   232  	}
   233  	if err := w.semaphore.Acquire(ctx, semaphoreWeight); err != nil {
   234  		return xerrors.WithStackTrace(
   235  			fmt.Errorf("ydb: add new messages exceed max queue size limit. Add count: %v, max size: %v: %w",
   236  				semaphoreWeight,
   237  				w.cfg.MaxQueueLen,
   238  				PublicErrQueueIsFull,
   239  			))
   240  	}
   241  	defer func() {
   242  		w.semaphore.Release(semaphoreWeight)
   243  	}()
   244  
   245  	messagesSlice, err := w.createMessagesWithContent(messages)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	if err = w.checkMessages(messagesSlice); err != nil {
   251  		return err
   252  	}
   253  
   254  	if err = w.waitFirstInitResponse(ctx); err != nil {
   255  		return err
   256  	}
   257  
   258  	waiter, err := w.addMessageToInternalQueueWithLock(messagesSlice, &semaphoreWeight)
   259  	if err != nil {
   260  		return err
   261  	}
   262  	defer func() {
   263  		if resErr != nil {
   264  			resErr = xerrors.Join(resErr, PublicErrMessagesPutToInternalQueueBeforeError)
   265  		}
   266  	}()
   267  
   268  	if !w.cfg.WaitServerAck {
   269  		return nil
   270  	}
   271  
   272  	return w.queue.Wait(ctx, waiter)
   273  }
   274  
   275  func (w *WriterReconnector) addMessageToInternalQueueWithLock(
   276  	messagesSlice []messageWithDataContent,
   277  	semaphoreWeight *int64,
   278  ) (MessageQueueAckWaiter, error) {
   279  	var (
   280  		waiter MessageQueueAckWaiter
   281  		err    error
   282  	)
   283  	w.m.WithLock(func() {
   284  		// need set numbers and add to queue atomically
   285  		err = w.fillFields(messagesSlice)
   286  		if err != nil {
   287  			return
   288  		}
   289  
   290  		if w.cfg.WaitServerAck {
   291  			waiter, err = w.queue.AddMessagesWithWaiter(messagesSlice)
   292  		} else {
   293  			err = w.queue.AddMessages(messagesSlice)
   294  		}
   295  		if err == nil {
   296  			// move semaphore weight to queue
   297  			*semaphoreWeight = 0
   298  		}
   299  	})
   300  
   301  	return waiter, err
   302  }
   303  
   304  func (w *WriterReconnector) checkMessages(messages []messageWithDataContent) error {
   305  	for i := range messages {
   306  		size := messages[i].BufUncompressedSize
   307  		if size > w.cfg.MaxMessageSize {
   308  			return xerrors.WithStackTrace(fmt.Errorf("message size bytes %v: %w", size, errLargeMessage))
   309  		}
   310  	}
   311  
   312  	return nil
   313  }
   314  
   315  func (w *WriterReconnector) createMessagesWithContent(messages []PublicMessage) ([]messageWithDataContent, error) {
   316  	res := make([]messageWithDataContent, 0, len(messages))
   317  	for i := range messages {
   318  		mess := newMessageDataWithContent(messages[i], w.encodersMap)
   319  		res = append(res, mess)
   320  	}
   321  
   322  	var sessionID string
   323  	w.m.WithRLock(func() {
   324  		sessionID = w.sessionID
   325  	})
   326  	onCompressDone := trace.TopicOnWriterCompressMessages(
   327  		w.cfg.Tracer,
   328  		w.writerInstanceID,
   329  		sessionID,
   330  		w.cfg.forceCodec.ToInt32(),
   331  		messages[0].SeqNo,
   332  		len(messages),
   333  		trace.TopicWriterCompressMessagesReasonCompressDataOnWriteReadData,
   334  	)
   335  
   336  	targetCodec := w.cfg.forceCodec
   337  	if targetCodec == rawtopiccommon.CodecUNSPECIFIED {
   338  		targetCodec = rawtopiccommon.CodecRaw
   339  	}
   340  	err := cacheMessages(res, targetCodec, w.cfg.compressorCount)
   341  	onCompressDone(err)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	return res, nil
   347  }
   348  
   349  func (w *WriterReconnector) Flush(ctx context.Context) error {
   350  	return w.queue.WaitLastWritten(ctx)
   351  }
   352  
   353  func (w *WriterReconnector) Close(ctx context.Context) error {
   354  	reason := xerrors.WithStackTrace(errStopWriterReconnector)
   355  	w.queue.StopAddNewMessages(reason)
   356  
   357  	flushErr := w.Flush(ctx) //nolint:ifshort,nolintlint
   358  	closeErr := w.close(ctx, reason)
   359  
   360  	if flushErr != nil {
   361  		return flushErr
   362  	}
   363  
   364  	return closeErr
   365  }
   366  
   367  func (w *WriterReconnector) close(ctx context.Context, reason error) (resErr error) {
   368  	onDone := trace.TopicOnWriterClose(w.cfg.Tracer, w.writerInstanceID, reason)
   369  	defer func() {
   370  		onDone(resErr)
   371  	}()
   372  
   373  	// stop background work and single stream writer
   374  	bgErr := w.background.Close(ctx, reason)
   375  	if resErr == nil && bgErr != nil {
   376  		resErr = bgErr
   377  	}
   378  
   379  	closeErr := w.queue.Close(reason)
   380  	if resErr == nil && closeErr != nil {
   381  		resErr = closeErr
   382  	}
   383  
   384  	return resErr
   385  }
   386  
   387  func (w *WriterReconnector) connectionLoop(ctx context.Context) {
   388  	attempt := 0
   389  
   390  	createStreamContext := func() (context.Context, context.CancelFunc) {
   391  		// need suppress parent context cancelation for flush buffer while close writer
   392  		return xcontext.WithCancel(xcontext.ValueOnly(ctx))
   393  	}
   394  
   395  	//nolint:ineffassign,staticcheck,wastedassign
   396  	streamCtx, streamCtxCancel := createStreamContext()
   397  
   398  	defer streamCtxCancel()
   399  
   400  	var reconnectReason error
   401  	var prevAttemptTime time.Time
   402  	var startOfRetries time.Time
   403  
   404  	for {
   405  		if ctx.Err() != nil {
   406  			return
   407  		}
   408  
   409  		streamCtxCancel()
   410  		streamCtx, streamCtxCancel = createStreamContext()
   411  
   412  		now := time.Now()
   413  		if startOfRetries.IsZero() || topic.CheckResetReconnectionCounters(prevAttemptTime, now, w.cfg.connectTimeout) {
   414  			attempt = 0
   415  			startOfRetries = w.cfg.clock.Now()
   416  		} else {
   417  			attempt++
   418  		}
   419  		prevAttemptTime = now
   420  
   421  		if reconnectReason != nil {
   422  			if w.handleReconnectRetry(ctx, reconnectReason, attempt, startOfRetries) {
   423  				return
   424  			}
   425  		}
   426  
   427  		writer, err := w.startWriteStream(ctx, streamCtx, attempt)
   428  		w.onWriterChange(writer)
   429  		if err == nil {
   430  			reconnectReason = writer.WaitClose(ctx)
   431  			startOfRetries = time.Now()
   432  		} else {
   433  			reconnectReason = err
   434  		}
   435  	}
   436  }
   437  
   438  func (w *WriterReconnector) handleReconnectRetry(
   439  	ctx context.Context,
   440  	reconnectReason error,
   441  	attempt int,
   442  	startOfRetries time.Time,
   443  ) bool {
   444  	retryDuration := w.cfg.clock.Since(startOfRetries)
   445  	if backoff, stopRetryReason := topic.RetryDecision(
   446  		reconnectReason,
   447  		w.retrySettings,
   448  		retryDuration,
   449  	); stopRetryReason == nil {
   450  		delay := backoff.Delay(attempt)
   451  		delayTimer := w.cfg.clock.NewTimer(delay)
   452  		select {
   453  		case <-ctx.Done():
   454  			delayTimer.Stop()
   455  
   456  			return true
   457  		case <-delayTimer.Chan():
   458  			delayTimer.Stop() // no really need, stop for common style only
   459  			// pass
   460  		}
   461  	} else {
   462  		_ = w.close(ctx, fmt.Errorf("%w, was retried (%v)", stopRetryReason, retryDuration))
   463  
   464  		return true
   465  	}
   466  
   467  	return false
   468  }
   469  
   470  func (w *WriterReconnector) startWriteStream(ctx, streamCtx context.Context, attempt int) (
   471  	writer *SingleStreamWriter,
   472  	err error,
   473  ) {
   474  	traceOnDone := trace.TopicOnWriterReconnect(
   475  		w.cfg.Tracer,
   476  		w.writerInstanceID,
   477  		w.cfg.topic,
   478  		w.cfg.producerID,
   479  		attempt,
   480  	)
   481  	defer func() {
   482  		traceOnDone(err)
   483  	}()
   484  
   485  	stream, err := w.connectWithTimeout(streamCtx)
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	w.queue.ResetSentProgress()
   491  
   492  	return NewSingleStreamWriter(ctx, w.createWriterStreamConfig(stream))
   493  }
   494  
   495  func (w *WriterReconnector) needReceiveLastSeqNo() bool {
   496  	res := !w.firstConnectionHandled.Load()
   497  
   498  	return res
   499  }
   500  
   501  func (w *WriterReconnector) connectWithTimeout(streamLifetimeContext context.Context) (RawTopicWriterStream, error) {
   502  	connectCtx, connectCancel := xcontext.WithCancel(streamLifetimeContext)
   503  
   504  	type resT struct {
   505  		stream RawTopicWriterStream
   506  		err    error
   507  	}
   508  	resCh := make(chan resT, 1)
   509  
   510  	go func() {
   511  		defer func() {
   512  			p := recover()
   513  			if p != nil {
   514  				resCh <- resT{
   515  					stream: nil,
   516  					err:    xerrors.WithStackTrace(xerrors.Wrap(fmt.Errorf("ydb: panic while connect to topic writer: %+v", p))),
   517  				}
   518  			}
   519  		}()
   520  
   521  		stream, err := w.cfg.Connect(connectCtx)
   522  		resCh <- resT{stream: stream, err: err}
   523  	}()
   524  
   525  	timer := time.NewTimer(w.cfg.connectTimeout)
   526  	defer timer.Stop()
   527  
   528  	select {
   529  	case <-timer.C:
   530  		connectCancel()
   531  
   532  		return nil, xerrors.WithStackTrace(errConnTimeout)
   533  	case res := <-resCh:
   534  		// force no cancel connect context - because it will break stream
   535  		// context will cancel by cancel streamLifetimeContext while reconnect or stop connection
   536  		_ = connectCancel
   537  
   538  		return res.stream, res.err
   539  	}
   540  }
   541  
   542  func (w *WriterReconnector) onAckReceived(count int) {
   543  	w.semaphore.Release(int64(count))
   544  }
   545  
   546  func (w *WriterReconnector) onWriterChange(writerStream *SingleStreamWriter) {
   547  	isFirstInit := false
   548  	w.m.WithLock(func() {
   549  		if writerStream == nil {
   550  			w.sessionID = ""
   551  
   552  			return
   553  		}
   554  		w.sessionID = writerStream.SessionID
   555  
   556  		if !w.firstConnectionHandled.CompareAndSwap(false, true) {
   557  			return
   558  		}
   559  		defer close(w.firstInitResponseProcessedChan)
   560  		isFirstInit = true
   561  
   562  		if writerStream.LastSeqNumRequested {
   563  			w.lastSeqNo = writerStream.ReceivedLastSeqNum
   564  		}
   565  	})
   566  
   567  	if isFirstInit {
   568  		w.m.WithLock(func() {
   569  			w.initDone = true
   570  			w.initInfo = InitialInfo{LastSeqNum: w.lastSeqNo}
   571  			close(w.initDoneCh)
   572  		})
   573  		w.onWriterInitCallbackHandler(writerStream)
   574  	}
   575  }
   576  
   577  func (w *WriterReconnector) WaitInit(ctx context.Context) (info InitialInfo, err error) {
   578  	if ctx.Err() != nil {
   579  		return InitialInfo{}, ctx.Err()
   580  	}
   581  
   582  	select {
   583  	case <-ctx.Done():
   584  		return InitialInfo{}, ctx.Err()
   585  	case <-w.background.Done():
   586  		return InitialInfo{}, w.background.CloseReason()
   587  	case <-w.initDoneCh:
   588  		return w.initInfo, nil
   589  	}
   590  }
   591  
   592  func (w *WriterReconnector) onWriterInitCallbackHandler(writerStream *SingleStreamWriter) {
   593  	if w.cfg.OnWriterInitResponseCallback != nil {
   594  		info := PublicWithOnWriterConnectedInfo{
   595  			LastSeqNo:        w.lastSeqNo,
   596  			SessionID:        w.sessionID,
   597  			PartitionID:      writerStream.PartitionID,
   598  			CodecsFromServer: createPublicCodecsFromRaw(writerStream.CodecsFromServer),
   599  		}
   600  
   601  		if err := w.cfg.OnWriterInitResponseCallback(info); err != nil {
   602  			_ = w.close(context.Background(), fmt.Errorf("OnWriterInitResponseCallback return error: %w", err))
   603  		}
   604  	}
   605  }
   606  
   607  func (w *WriterReconnector) waitFirstInitResponse(ctx context.Context) error {
   608  	if err := ctx.Err(); err != nil {
   609  		return err
   610  	}
   611  
   612  	if w.firstConnectionHandled.Load() {
   613  		return nil
   614  	}
   615  
   616  	select {
   617  	case <-w.background.Done():
   618  		return w.background.CloseReason()
   619  	case <-w.firstInitResponseProcessedChan:
   620  		return nil
   621  	case <-ctx.Done():
   622  		return ctx.Err()
   623  	}
   624  }
   625  
   626  func (w *WriterReconnector) createWriterStreamConfig(stream RawTopicWriterStream) SingleStreamWriterConfig {
   627  	cfg := newSingleStreamWriterConfig(
   628  		w.cfg.WritersCommonConfig,
   629  		stream,
   630  		&w.queue,
   631  		w.encodersMap,
   632  		w.needReceiveLastSeqNo(),
   633  		w.writerInstanceID,
   634  	)
   635  
   636  	return cfg
   637  }
   638  
   639  func (w *WriterReconnector) GetSessionID() (sessionID string) {
   640  	w.m.WithLock(func() {
   641  		sessionID = w.sessionID
   642  	})
   643  
   644  	return sessionID
   645  }
   646  
   647  func sendMessagesToStream(
   648  	stream RawTopicWriterStream,
   649  	targetCodec rawtopiccommon.Codec,
   650  	messages []messageWithDataContent,
   651  ) error {
   652  	if len(messages) == 0 {
   653  		return nil
   654  	}
   655  
   656  	request, err := createWriteRequest(messages, targetCodec)
   657  	if err != nil {
   658  		return err
   659  	}
   660  	err = stream.Send(&request)
   661  	if err != nil {
   662  		return xerrors.WithStackTrace(fmt.Errorf("ydb: failed send write request: %w", err))
   663  	}
   664  
   665  	return nil
   666  }
   667  
   668  func allMessagesHasSameBufCodec(messages []messageWithDataContent) bool {
   669  	if len(messages) <= 1 {
   670  		return true
   671  	}
   672  
   673  	codec := messages[0].bufCodec
   674  	for i := range messages {
   675  		if messages[i].bufCodec != codec {
   676  			return false
   677  		}
   678  	}
   679  
   680  	return true
   681  }
   682  
   683  func splitMessagesByBufCodec(messages []messageWithDataContent) (res [][]messageWithDataContent) {
   684  	if len(messages) == 0 {
   685  		return nil
   686  	}
   687  
   688  	currentGroupStart := 0
   689  	currentCodec := messages[0].bufCodec
   690  	for i := range messages {
   691  		if messages[i].bufCodec != currentCodec {
   692  			res = append(res, messages[currentGroupStart:i:i])
   693  			currentGroupStart = i
   694  			currentCodec = messages[i].bufCodec
   695  		}
   696  	}
   697  	res = append(res, messages[currentGroupStart:len(messages):len(messages)])
   698  
   699  	return res
   700  }
   701  
   702  func createWriteRequest(messages []messageWithDataContent, targetCodec rawtopiccommon.Codec) (
   703  	res rawtopicwriter.WriteRequest,
   704  	err error,
   705  ) {
   706  	for i := 1; i < len(messages); i++ {
   707  		if messages[i-1].tx != messages[i].tx {
   708  			return res, xerrors.WithStackTrace(errDiffetentTransactions)
   709  		}
   710  	}
   711  
   712  	if len(messages) > 0 && messages[0].tx != nil {
   713  		res.Tx.ID = messages[0].tx.ID()
   714  		res.Tx.Session = messages[0].tx.SessionID()
   715  	}
   716  
   717  	res.Codec = targetCodec
   718  	res.Messages = make([]rawtopicwriter.MessageData, len(messages))
   719  	for i := range messages {
   720  		res.Messages[i], err = createRawMessageData(res.Codec, &messages[i])
   721  		if err != nil {
   722  			return res, err
   723  		}
   724  	}
   725  
   726  	return res, nil
   727  }
   728  
   729  func createRawMessageData(
   730  	codec rawtopiccommon.Codec,
   731  	mess *messageWithDataContent,
   732  ) (res rawtopicwriter.MessageData, err error) {
   733  	res.CreatedAt = mess.CreatedAt
   734  	res.SeqNo = mess.SeqNo
   735  
   736  	switch {
   737  	case mess.futurePartitioning.hasPartitionID:
   738  		res.Partitioning.Type = rawtopicwriter.PartitioningPartitionID
   739  		res.Partitioning.PartitionID = mess.futurePartitioning.partitionID
   740  	case mess.futurePartitioning.messageGroupID != "":
   741  		res.Partitioning.Type = rawtopicwriter.PartitioningMessageGroupID
   742  		res.Partitioning.MessageGroupID = mess.futurePartitioning.messageGroupID
   743  	default:
   744  		// pass
   745  	}
   746  
   747  	res.UncompressedSize = int64(mess.BufUncompressedSize)
   748  	res.Data, err = mess.GetEncodedBytes(codec)
   749  
   750  	if len(mess.Metadata) > 0 {
   751  		res.MetadataItems = make([]rawtopiccommon.MetadataItem, 0, len(mess.Metadata))
   752  		for key, val := range mess.Metadata {
   753  			res.MetadataItems = append(res.MetadataItems, rawtopiccommon.MetadataItem{
   754  				Key:   key,
   755  				Value: val,
   756  			})
   757  		}
   758  	}
   759  
   760  	return res, err
   761  }
   762  
   763  func calculateAllowedCodecs(forceCodec rawtopiccommon.Codec, encoderMap *EncoderMap,
   764  	serverCodecs rawtopiccommon.SupportedCodecs,
   765  ) rawtopiccommon.SupportedCodecs {
   766  	if forceCodec != rawtopiccommon.CodecUNSPECIFIED {
   767  		if serverCodecs.AllowedByCodecsList(forceCodec) && encoderMap.IsSupported(forceCodec) {
   768  			return rawtopiccommon.SupportedCodecs{forceCodec}
   769  		}
   770  
   771  		return nil
   772  	}
   773  
   774  	if len(serverCodecs) == 0 {
   775  		// fixed list for autoselect codec if empty server list for prevent unexpectedly add messages with new codec
   776  		// with sdk update
   777  		serverCodecs = rawtopiccommon.SupportedCodecs{rawtopiccommon.CodecRaw, rawtopiccommon.CodecGzip}
   778  	}
   779  
   780  	res := make(rawtopiccommon.SupportedCodecs, 0, len(serverCodecs))
   781  	for _, codec := range serverCodecs {
   782  		if encoderMap.IsSupported(codec) {
   783  			res = append(res, codec)
   784  		}
   785  	}
   786  	if len(res) == 0 {
   787  		res = nil
   788  	}
   789  
   790  	return res
   791  }
   792  
   793  type ConnectFunc func(ctx context.Context) (RawTopicWriterStream, error)
   794  
   795  func createPublicCodecsFromRaw(codecs rawtopiccommon.SupportedCodecs) []topictypes.Codec {
   796  	res := make([]topictypes.Codec, len(codecs))
   797  	for i, v := range codecs {
   798  		res[i] = topictypes.Codec(v)
   799  	}
   800  
   801  	return res
   802  }