github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/internal/topic/topicreaderinternal/stream_reader_impl.go (about)

     1  package topicreaderinternal
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"math/big"
    10  	"reflect"
    11  	"runtime/pprof"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/ydb-platform/ydb-go-sdk/v3/credentials"
    16  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/background"
    17  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon"
    18  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader"
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    20  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    21  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    23  )
    24  
    25  var (
    26  	PublicErrCommitSessionToExpiredSession = xerrors.Wrap(errors.New("ydb: commit to expired session"))
    27  
    28  	errCommitWithNilPartitionSession = xerrors.Wrap(errors.New("ydb: commit with nil partition session"))
    29  )
    30  
    31  type partitionSessionID = rawtopicreader.PartitionSessionID
    32  
    33  type topicStreamReaderImpl struct {
    34  	cfg    topicStreamReaderConfig
    35  	ctx    context.Context
    36  	cancel context.CancelFunc
    37  
    38  	freeBytes           chan int
    39  	restBufferSizeBytes atomic.Int64
    40  	sessionController   partitionSessionStorage
    41  	backgroundWorkers   background.Worker
    42  
    43  	rawMessagesFromBuffer chan rawtopicreader.ServerMessage
    44  
    45  	batcher   *batcher
    46  	committer *committer
    47  
    48  	stream           RawTopicReaderStream
    49  	readConnectionID string
    50  	readerID         int64
    51  
    52  	m       xsync.RWMutex
    53  	err     error
    54  	started bool
    55  	closed  bool
    56  }
    57  
    58  type topicStreamReaderConfig struct {
    59  	CommitterBatchTimeLag           time.Duration
    60  	CommitterBatchCounterTrigger    int
    61  	BaseContext                     context.Context
    62  	BufferSizeProtoBytes            int
    63  	Cred                            credentials.Credentials
    64  	CredUpdateInterval              time.Duration
    65  	Consumer                        string
    66  	ReadSelectors                   []*PublicReadSelector
    67  	Trace                           *trace.Topic
    68  	GetPartitionStartOffsetCallback PublicGetPartitionStartOffsetFunc
    69  	CommitMode                      PublicCommitMode
    70  	Decoders                        decoderMap
    71  }
    72  
    73  func newTopicStreamReaderConfig() topicStreamReaderConfig {
    74  	return topicStreamReaderConfig{
    75  		BaseContext:           context.Background(),
    76  		BufferSizeProtoBytes:  1024 * 1024,
    77  		Cred:                  credentials.NewAnonymousCredentials(),
    78  		CredUpdateInterval:    time.Hour,
    79  		CommitMode:            CommitModeAsync,
    80  		CommitterBatchTimeLag: time.Second,
    81  		Decoders:              newDecoderMap(),
    82  		Trace:                 &trace.Topic{},
    83  	}
    84  }
    85  
    86  func (cfg *topicStreamReaderConfig) initMessage() *rawtopicreader.InitRequest {
    87  	res := &rawtopicreader.InitRequest{
    88  		Consumer: cfg.Consumer,
    89  	}
    90  
    91  	res.TopicsReadSettings = make([]rawtopicreader.TopicReadSettings, len(cfg.ReadSelectors))
    92  	for i, selector := range cfg.ReadSelectors {
    93  		settings := &res.TopicsReadSettings[i]
    94  		settings.Path = selector.Path
    95  		settings.PartitionsID = selector.Partitions
    96  		if !selector.ReadFrom.IsZero() {
    97  			settings.ReadFrom.HasValue = true
    98  			settings.ReadFrom.Value = selector.ReadFrom
    99  		}
   100  		if selector.MaxTimeLag != 0 {
   101  			settings.MaxLag.HasValue = true
   102  			settings.MaxLag.Value = selector.MaxTimeLag
   103  		}
   104  	}
   105  
   106  	return res
   107  }
   108  
   109  func newTopicStreamReader(
   110  	readerID int64,
   111  	stream RawTopicReaderStream,
   112  	cfg topicStreamReaderConfig, //nolint:gocritic
   113  ) (_ *topicStreamReaderImpl, err error) {
   114  	defer func() {
   115  		if err != nil {
   116  			_ = stream.CloseSend()
   117  		}
   118  	}()
   119  
   120  	reader := newTopicStreamReaderStopped(readerID, stream, cfg)
   121  	if err = reader.initSession(); err != nil {
   122  		return nil, err
   123  	}
   124  	if err = reader.startLoops(); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	return reader, nil
   129  }
   130  
   131  func newTopicStreamReaderStopped(
   132  	readerID int64,
   133  	stream RawTopicReaderStream,
   134  	cfg topicStreamReaderConfig, //nolint:gocritic
   135  ) *topicStreamReaderImpl {
   136  	labeledContext := pprof.WithLabels(cfg.BaseContext, pprof.Labels("base-context", "topic-stream-reader"))
   137  	stopPump, cancel := xcontext.WithCancel(labeledContext)
   138  
   139  	readerConnectionID, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
   140  	if err != nil {
   141  		readerConnectionID = big.NewInt(-1)
   142  	}
   143  
   144  	res := &topicStreamReaderImpl{
   145  		cfg:                   cfg,
   146  		ctx:                   stopPump,
   147  		freeBytes:             make(chan int, 1),
   148  		stream:                &syncedStream{stream: stream},
   149  		cancel:                cancel,
   150  		batcher:               newBatcher(),
   151  		backgroundWorkers:     *background.NewWorker(stopPump),
   152  		readConnectionID:      "preinitID-" + readerConnectionID.String(),
   153  		readerID:              readerID,
   154  		rawMessagesFromBuffer: make(chan rawtopicreader.ServerMessage, 1),
   155  	}
   156  
   157  	res.committer = newCommitter(cfg.Trace, labeledContext, cfg.CommitMode, res.send)
   158  	res.committer.BufferTimeLagTrigger = cfg.CommitterBatchTimeLag
   159  	res.committer.BufferCountTrigger = cfg.CommitterBatchCounterTrigger
   160  	res.sessionController.init()
   161  	res.freeBytes <- cfg.BufferSizeProtoBytes
   162  
   163  	return res
   164  }
   165  
   166  func (r *topicStreamReaderImpl) WaitInit(_ context.Context) error {
   167  	if !r.started {
   168  		return errors.New("not started: can be started only after initialize from constructor")
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  func (r *topicStreamReaderImpl) ReadMessageBatch(
   175  	ctx context.Context,
   176  	opts ReadMessageBatchOptions,
   177  ) (batch *PublicBatch, err error) {
   178  	onDone := trace.TopicOnReaderReadMessages(
   179  		r.cfg.Trace,
   180  		ctx,
   181  		opts.MinCount,
   182  		opts.MaxCount,
   183  		r.getRestBufferBytes(),
   184  	)
   185  	defer func() {
   186  		if batch == nil {
   187  			onDone(0, "", -1, -1, -1, -1, r.getRestBufferBytes(), err)
   188  		} else {
   189  			onDone(
   190  				len(batch.Messages),
   191  				batch.Topic(),
   192  				batch.PartitionID(),
   193  				batch.partitionSession().partitionSessionID.ToInt64(),
   194  				batch.commitRange.commitOffsetStart.ToInt64(),
   195  				batch.commitRange.commitOffsetEnd.ToInt64(),
   196  				r.getRestBufferBytes(),
   197  				err,
   198  			)
   199  		}
   200  	}()
   201  
   202  	if err = ctx.Err(); err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	defer func() {
   207  		if err == nil {
   208  			r.freeBufferFromMessages(batch)
   209  		}
   210  	}()
   211  
   212  	return r.consumeMessagesUntilBatch(ctx, opts)
   213  }
   214  
   215  func (r *topicStreamReaderImpl) consumeMessagesUntilBatch(
   216  	ctx context.Context,
   217  	opts ReadMessageBatchOptions,
   218  ) (*PublicBatch, error) {
   219  	for {
   220  		item, err := r.batcher.Pop(ctx, opts.batcherGetOptions)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  
   225  		switch {
   226  		case item.IsBatch():
   227  			return item.Batch, nil
   228  		case item.IsRawMessage():
   229  			r.sendRawMessageToChannelUnblocked(item.RawMessage)
   230  		default:
   231  			return nil, xerrors.WithStackTrace(fmt.Errorf("ydb: unexpected item type from batcher: %#v", item))
   232  		}
   233  	}
   234  }
   235  
   236  func (r *topicStreamReaderImpl) sendRawMessageToChannelUnblocked(msg rawtopicreader.ServerMessage) {
   237  	select {
   238  	case r.rawMessagesFromBuffer <- msg:
   239  		return
   240  	default:
   241  		// send in goroutine, without block caller
   242  		r.backgroundWorkers.Start("sendMessageToRawChannel", func(ctx context.Context) {
   243  			select {
   244  			case r.rawMessagesFromBuffer <- msg:
   245  			case <-ctx.Done():
   246  			}
   247  		})
   248  	}
   249  }
   250  
   251  func (r *topicStreamReaderImpl) consumeRawMessageFromBuffer(ctx context.Context) {
   252  	doneChan := ctx.Done()
   253  
   254  	for {
   255  		var msg rawtopicreader.ServerMessage
   256  		select {
   257  		case <-doneChan:
   258  			return
   259  		case msg = <-r.rawMessagesFromBuffer:
   260  			// pass
   261  		}
   262  
   263  		switch m := msg.(type) {
   264  		case *rawtopicreader.StartPartitionSessionRequest:
   265  			if err := r.onStartPartitionSessionRequestFromBuffer(m); err != nil {
   266  				_ = r.CloseWithError(ctx, err)
   267  
   268  				return
   269  			}
   270  		case *rawtopicreader.StopPartitionSessionRequest:
   271  			if err := r.onStopPartitionSessionRequestFromBuffer(m); err != nil {
   272  				_ = r.CloseWithError(ctx, xerrors.WithStackTrace(
   273  					fmt.Errorf("ydb: unexpected error on stop partition handler: %w", err),
   274  				))
   275  
   276  				return
   277  			}
   278  		case *rawtopicreader.PartitionSessionStatusResponse:
   279  			r.onPartitionSessionStatusResponseFromBuffer(ctx, m)
   280  		default:
   281  			_ = r.CloseWithError(ctx, xerrors.WithStackTrace(
   282  				fmt.Errorf("ydb: unexpected server message from buffer: %v", reflect.TypeOf(msg))),
   283  			)
   284  		}
   285  	}
   286  }
   287  
   288  func (r *topicStreamReaderImpl) onStopPartitionSessionRequestFromBuffer(
   289  	msg *rawtopicreader.StopPartitionSessionRequest,
   290  ) (err error) {
   291  	session, err := r.sessionController.Get(msg.PartitionSessionID)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	onDone := trace.TopicOnReaderPartitionReadStopResponse(
   297  		r.cfg.Trace,
   298  		r.readConnectionID,
   299  		session.Context(),
   300  		session.Topic,
   301  		session.PartitionID,
   302  		session.partitionSessionID.ToInt64(),
   303  		msg.CommittedOffset.ToInt64(),
   304  		msg.Graceful,
   305  	)
   306  	defer func() {
   307  		onDone(err)
   308  	}()
   309  
   310  	if msg.Graceful {
   311  		session.Close()
   312  		resp := &rawtopicreader.StopPartitionSessionResponse{
   313  			PartitionSessionID: session.partitionSessionID,
   314  		}
   315  		if err = r.send(resp); err != nil {
   316  			return err
   317  		}
   318  	}
   319  
   320  	if _, err = r.sessionController.Remove(session.partitionSessionID); err != nil {
   321  		if msg.Graceful {
   322  			return err
   323  		} else { //nolint:revive,staticcheck
   324  			// double message with graceful=false is ok.
   325  			// It may be received after message with graceful=true and session was removed while process that.
   326  
   327  			// pass
   328  		}
   329  	}
   330  
   331  	return nil
   332  }
   333  
   334  func (r *topicStreamReaderImpl) onPartitionSessionStatusResponseFromBuffer(
   335  	ctx context.Context,
   336  	m *rawtopicreader.PartitionSessionStatusResponse,
   337  ) {
   338  	panic("not implemented")
   339  }
   340  
   341  func (r *topicStreamReaderImpl) onUpdateTokenResponse(m *rawtopicreader.UpdateTokenResponse) {
   342  }
   343  
   344  func (r *topicStreamReaderImpl) Commit(ctx context.Context, commitRange commitRange) (err error) {
   345  	defer func() {
   346  		if errors.Is(err, PublicErrCommitSessionToExpiredSession) && r.cfg.CommitMode == CommitModeAsync {
   347  			err = nil
   348  		}
   349  	}()
   350  
   351  	if commitRange.partitionSession == nil {
   352  		return xerrors.WithStackTrace(errCommitWithNilPartitionSession)
   353  	}
   354  
   355  	session := commitRange.partitionSession
   356  	onDone := trace.TopicOnReaderCommit(
   357  		r.cfg.Trace,
   358  		ctx,
   359  		session.Topic,
   360  		session.PartitionID,
   361  		session.partitionSessionID.ToInt64(),
   362  		commitRange.commitOffsetStart.ToInt64(),
   363  		commitRange.commitOffsetEnd.ToInt64(),
   364  	)
   365  	defer func() {
   366  		onDone(err)
   367  	}()
   368  
   369  	if err = r.checkCommitRange(commitRange); err != nil {
   370  		return err
   371  	}
   372  
   373  	return r.committer.Commit(ctx, commitRange)
   374  }
   375  
   376  func (r *topicStreamReaderImpl) checkCommitRange(commitRange commitRange) error {
   377  	if r.cfg.CommitMode == CommitModeNone {
   378  		return ErrCommitDisabled
   379  	}
   380  	session := commitRange.partitionSession
   381  
   382  	if session == nil {
   383  		return xerrors.WithStackTrace(errCommitWithNilPartitionSession)
   384  	}
   385  
   386  	if session.Context().Err() != nil {
   387  		return xerrors.WithStackTrace(PublicErrCommitSessionToExpiredSession)
   388  	}
   389  
   390  	ownSession, err := r.sessionController.Get(session.partitionSessionID)
   391  	if err != nil || session != ownSession {
   392  		return xerrors.WithStackTrace(PublicErrCommitSessionToExpiredSession)
   393  	}
   394  	if session.committedOffset() != commitRange.commitOffsetStart && r.cfg.CommitMode == CommitModeSync {
   395  		return ErrWrongCommitOrderInSyncMode
   396  	}
   397  
   398  	return nil
   399  }
   400  
   401  func (r *topicStreamReaderImpl) send(msg rawtopicreader.ClientMessage) error {
   402  	err := r.stream.Send(msg)
   403  	if err != nil {
   404  		trace.TopicOnReaderError(r.cfg.Trace, r.readConnectionID, err)
   405  		_ = r.CloseWithError(r.ctx, err)
   406  	}
   407  
   408  	return err
   409  }
   410  
   411  func (r *topicStreamReaderImpl) startLoops() error {
   412  	if err := r.setStarted(); err != nil {
   413  		return err
   414  	}
   415  
   416  	r.backgroundWorkers.Start("readMessagesLoop", r.readMessagesLoop)
   417  	r.backgroundWorkers.Start("dataRequestLoop", r.dataRequestLoop)
   418  	r.backgroundWorkers.Start("updateTokenLoop", r.updateTokenLoop)
   419  
   420  	r.backgroundWorkers.Start("consumeRawMessageFromBuffer", r.consumeRawMessageFromBuffer)
   421  
   422  	return nil
   423  }
   424  
   425  func (r *topicStreamReaderImpl) setStarted() error {
   426  	r.m.Lock()
   427  	defer r.m.Unlock()
   428  
   429  	if r.started {
   430  		return xerrors.WithStackTrace(errors.New("already started"))
   431  	}
   432  
   433  	r.started = true
   434  
   435  	return nil
   436  }
   437  
   438  func (r *topicStreamReaderImpl) initSession() (err error) {
   439  	initMessage := r.cfg.initMessage()
   440  
   441  	onDone := trace.TopicOnReaderInit(r.cfg.Trace, r.readConnectionID, initMessage)
   442  	defer func() {
   443  		onDone(r.readConnectionID, err)
   444  	}()
   445  
   446  	if err = r.send(initMessage); err != nil {
   447  		return err
   448  	}
   449  
   450  	resp, err := r.stream.Recv()
   451  	if err != nil {
   452  		return err
   453  	}
   454  
   455  	if status := resp.StatusData(); !status.Status.IsSuccess() {
   456  		return xerrors.WithStackTrace(fmt.Errorf("bad status on initial error: %v (%v)", status.Status, status.Issues))
   457  	}
   458  
   459  	initResp, ok := resp.(*rawtopicreader.InitResponse)
   460  	if !ok {
   461  		return xerrors.WithStackTrace(fmt.Errorf("bad message type on session init: %v (%v)", resp, reflect.TypeOf(resp)))
   462  	}
   463  
   464  	r.readConnectionID = initResp.SessionID
   465  
   466  	return nil
   467  }
   468  
   469  func (r *topicStreamReaderImpl) addRestBufferBytes(delta int) int {
   470  	val := r.restBufferSizeBytes.Add(int64(delta))
   471  	if val <= 0 {
   472  		r.batcher.IgnoreMinRestrictionsOnNextPop()
   473  	}
   474  
   475  	return int(val)
   476  }
   477  
   478  func (r *topicStreamReaderImpl) getRestBufferBytes() int {
   479  	return int(r.restBufferSizeBytes.Load())
   480  }
   481  
   482  func (r *topicStreamReaderImpl) readMessagesLoop(ctx context.Context) {
   483  	ctx, cancel := xcontext.WithCancel(ctx)
   484  	defer cancel()
   485  
   486  	for {
   487  		serverMessage, err := r.stream.Recv()
   488  		if err != nil {
   489  			trace.TopicOnReaderError(r.cfg.Trace, r.readConnectionID, err)
   490  			if errors.Is(err, rawtopicreader.ErrUnexpectedMessageType) {
   491  				trace.TopicOnReaderUnknownGrpcMessage(r.cfg.Trace, r.readConnectionID, err)
   492  				// new messages can be added to protocol, it must be backward compatible to old programs
   493  				// and skip message is safe
   494  				continue
   495  			}
   496  			_ = r.CloseWithError(ctx, err)
   497  
   498  			return
   499  		}
   500  
   501  		status := serverMessage.StatusData()
   502  		if !status.Status.IsSuccess() {
   503  			_ = r.CloseWithError(ctx,
   504  				xerrors.WithStackTrace(
   505  					fmt.Errorf("ydb: bad status from pq grpc stream: %v, %v", status.Status, status.Issues.String()),
   506  				),
   507  			)
   508  		}
   509  
   510  		switch m := serverMessage.(type) {
   511  		case *rawtopicreader.ReadResponse:
   512  			if err = r.onReadResponse(m); err != nil {
   513  				_ = r.CloseWithError(ctx, err)
   514  			}
   515  		case *rawtopicreader.StartPartitionSessionRequest:
   516  			if err = r.onStartPartitionSessionRequest(m); err != nil {
   517  				_ = r.CloseWithError(ctx, err)
   518  
   519  				return
   520  			}
   521  		case *rawtopicreader.StopPartitionSessionRequest:
   522  			if err = r.onStopPartitionSessionRequest(m); err != nil {
   523  				_ = r.CloseWithError(ctx, err)
   524  
   525  				return
   526  			}
   527  		case *rawtopicreader.CommitOffsetResponse:
   528  			if err = r.onCommitResponse(m); err != nil {
   529  				_ = r.CloseWithError(ctx, err)
   530  
   531  				return
   532  			}
   533  
   534  		case *rawtopicreader.UpdateTokenResponse:
   535  			r.onUpdateTokenResponse(m)
   536  		default:
   537  			trace.TopicOnReaderUnknownGrpcMessage(
   538  				r.cfg.Trace,
   539  				r.readConnectionID,
   540  				xerrors.WithStackTrace(xerrors.Wrap(fmt.Errorf(
   541  					"ydb: unexpected message type in stream reader: %v",
   542  					reflect.TypeOf(serverMessage),
   543  				))),
   544  			)
   545  		}
   546  	}
   547  }
   548  
   549  func (r *topicStreamReaderImpl) dataRequestLoop(ctx context.Context) {
   550  	if r.ctx.Err() != nil {
   551  		return
   552  	}
   553  
   554  	doneChan := ctx.Done()
   555  
   556  	for {
   557  		select {
   558  		case <-doneChan:
   559  			_ = r.CloseWithError(ctx, r.ctx.Err())
   560  
   561  			return
   562  
   563  		case free := <-r.freeBytes:
   564  			sum := free
   565  
   566  			// consume all messages from order and compress it to one data request
   567  		forConsumeRequests:
   568  			for {
   569  				select {
   570  				case free = <-r.freeBytes:
   571  					sum += free
   572  				default:
   573  					break forConsumeRequests
   574  				}
   575  			}
   576  
   577  			resCapacity := r.addRestBufferBytes(sum)
   578  			trace.TopicOnReaderSentDataRequest(r.cfg.Trace, r.readConnectionID, sum, resCapacity)
   579  			if err := r.sendDataRequest(sum); err != nil {
   580  				return
   581  			}
   582  		}
   583  	}
   584  }
   585  
   586  func (r *topicStreamReaderImpl) sendDataRequest(size int) error {
   587  	return r.send(&rawtopicreader.ReadRequest{BytesSize: size})
   588  }
   589  
   590  func (r *topicStreamReaderImpl) freeBufferFromMessages(batch *PublicBatch) {
   591  	size := 0
   592  	for messageIndex := range batch.Messages {
   593  		size += batch.Messages[messageIndex].bufferBytesAccount
   594  	}
   595  	select {
   596  	case r.freeBytes <- size:
   597  	case <-r.ctx.Done():
   598  	}
   599  }
   600  
   601  func (r *topicStreamReaderImpl) updateTokenLoop(ctx context.Context) {
   602  	ticker := time.NewTicker(r.cfg.CredUpdateInterval)
   603  	defer ticker.Stop()
   604  
   605  	readerCancel := ctx.Done()
   606  	for {
   607  		select {
   608  		case <-readerCancel:
   609  			return
   610  		case <-ticker.C:
   611  			r.updateToken(r.ctx)
   612  		}
   613  	}
   614  }
   615  
   616  func (r *topicStreamReaderImpl) onReadResponse(msg *rawtopicreader.ReadResponse) (err error) {
   617  	resCapacity := r.addRestBufferBytes(-msg.BytesSize)
   618  	onDone := trace.TopicOnReaderReceiveDataResponse(r.cfg.Trace, r.readConnectionID, resCapacity, msg)
   619  	defer func() {
   620  		onDone(err)
   621  	}()
   622  
   623  	batchesCount := 0
   624  	for i := range msg.PartitionData {
   625  		batchesCount += len(msg.PartitionData[i].Batches)
   626  	}
   627  
   628  	var batches []*PublicBatch
   629  	for pIndex := range msg.PartitionData {
   630  		p := &msg.PartitionData[pIndex]
   631  
   632  		// normal way
   633  		session, err := r.sessionController.Get(p.PartitionSessionID)
   634  		if err != nil {
   635  			return err
   636  		}
   637  
   638  		for bIndex := range p.Batches {
   639  			if r.ctx.Err() != nil {
   640  				return r.ctx.Err()
   641  			}
   642  
   643  			batch, err := newBatchFromStream(r.cfg.Decoders, session, p.Batches[bIndex])
   644  			if err != nil {
   645  				return err
   646  			}
   647  			batches = append(batches, batch)
   648  		}
   649  	}
   650  
   651  	if err := splitBytesByMessagesInBatches(batches, msg.BytesSize); err != nil {
   652  		return err
   653  	}
   654  
   655  	for i := range batches {
   656  		if err := r.batcher.PushBatches(batches[i]); err != nil {
   657  			return err
   658  		}
   659  	}
   660  
   661  	return nil
   662  }
   663  
   664  func (r *topicStreamReaderImpl) CloseWithError(ctx context.Context, reason error) (closeErr error) {
   665  	onDone := trace.TopicOnReaderClose(r.cfg.Trace, r.readConnectionID, reason)
   666  	defer onDone(closeErr)
   667  
   668  	isFirstClose := false
   669  	r.m.WithLock(func() {
   670  		if r.closed {
   671  			return
   672  		}
   673  		isFirstClose = true
   674  		r.closed = true
   675  
   676  		r.err = reason
   677  		r.cancel()
   678  	})
   679  	if !isFirstClose {
   680  		return nil
   681  	}
   682  
   683  	closeErr = r.committer.Close(ctx, reason)
   684  
   685  	batcherErr := r.batcher.Close(reason)
   686  	if closeErr == nil {
   687  		closeErr = batcherErr
   688  	}
   689  
   690  	// close stream strong after committer close - for flush commits buffer
   691  	streamCloseErr := r.stream.CloseSend()
   692  	if closeErr == nil {
   693  		closeErr = streamCloseErr
   694  	}
   695  
   696  	// close background workers after r.stream.CloseSend
   697  	bgCloseErr := r.backgroundWorkers.Close(ctx, reason)
   698  	if closeErr == nil {
   699  		closeErr = bgCloseErr
   700  	}
   701  
   702  	return closeErr
   703  }
   704  
   705  func (r *topicStreamReaderImpl) onCommitResponse(msg *rawtopicreader.CommitOffsetResponse) error {
   706  	for i := range msg.PartitionsCommittedOffsets {
   707  		commit := &msg.PartitionsCommittedOffsets[i]
   708  		partition, err := r.sessionController.Get(commit.PartitionSessionID)
   709  		if err != nil {
   710  			return fmt.Errorf("ydb: can't found session on commit response: %w", err)
   711  		}
   712  		partition.setCommittedOffset(commit.CommittedOffset)
   713  
   714  		trace.TopicOnReaderCommittedNotify(
   715  			r.cfg.Trace,
   716  			r.readConnectionID,
   717  			partition.Topic,
   718  			partition.PartitionID,
   719  			partition.partitionSessionID.ToInt64(),
   720  			commit.CommittedOffset.ToInt64(),
   721  		)
   722  
   723  		r.committer.OnCommitNotify(partition, commit.CommittedOffset)
   724  	}
   725  
   726  	return nil
   727  }
   728  
   729  func (r *topicStreamReaderImpl) updateToken(ctx context.Context) {
   730  	onUpdateToken := trace.TopicOnReaderUpdateToken(
   731  		r.cfg.Trace,
   732  		r.readConnectionID,
   733  	)
   734  	token, err := r.cfg.Cred.Token(ctx)
   735  	onSent := onUpdateToken(len(token), err)
   736  	if err != nil {
   737  		return
   738  	}
   739  
   740  	err = r.send(&rawtopicreader.UpdateTokenRequest{UpdateTokenRequest: rawtopiccommon.UpdateTokenRequest{Token: token}})
   741  	onSent(err)
   742  }
   743  
   744  func (r *topicStreamReaderImpl) onStartPartitionSessionRequest(m *rawtopicreader.StartPartitionSessionRequest) error {
   745  	session := newPartitionSession(
   746  		r.ctx,
   747  		m.PartitionSession.Path,
   748  		m.PartitionSession.PartitionID,
   749  		r.readerID,
   750  		r.readConnectionID,
   751  		m.PartitionSession.PartitionSessionID,
   752  		m.CommittedOffset,
   753  	)
   754  	if err := r.sessionController.Add(session); err != nil {
   755  		return err
   756  	}
   757  
   758  	return r.batcher.PushRawMessage(session, m)
   759  }
   760  
   761  func (r *topicStreamReaderImpl) onStartPartitionSessionRequestFromBuffer(
   762  	m *rawtopicreader.StartPartitionSessionRequest,
   763  ) (err error) {
   764  	session, err := r.sessionController.Get(m.PartitionSession.PartitionSessionID)
   765  	if err != nil {
   766  		return err
   767  	}
   768  
   769  	onDone := trace.TopicOnReaderPartitionReadStartResponse(
   770  		r.cfg.Trace,
   771  		r.readConnectionID,
   772  		session.Context(),
   773  		session.Topic,
   774  		session.PartitionID,
   775  		session.partitionSessionID.ToInt64(),
   776  	)
   777  
   778  	respMessage := &rawtopicreader.StartPartitionSessionResponse{
   779  		PartitionSessionID: session.partitionSessionID,
   780  	}
   781  
   782  	var forceOffset *int64
   783  	var commitOffset *int64
   784  
   785  	defer func() {
   786  		onDone(forceOffset, commitOffset, err)
   787  	}()
   788  
   789  	if r.cfg.GetPartitionStartOffsetCallback != nil {
   790  		req := PublicGetPartitionStartOffsetRequest{
   791  			Topic:       session.Topic,
   792  			PartitionID: session.PartitionID,
   793  		}
   794  		resp, callbackErr := r.cfg.GetPartitionStartOffsetCallback(session.Context(), req)
   795  		if callbackErr != nil {
   796  			return callbackErr
   797  		}
   798  		if resp.startOffsetUsed {
   799  			wantOffset := resp.startOffset.ToInt64()
   800  			forceOffset = &wantOffset
   801  		}
   802  	}
   803  
   804  	respMessage.ReadOffset.FromInt64Pointer(forceOffset)
   805  	if r.cfg.CommitMode.commitsEnabled() {
   806  		commitOffset = forceOffset
   807  		respMessage.CommitOffset.FromInt64Pointer(commitOffset)
   808  	}
   809  
   810  	return r.send(respMessage)
   811  }
   812  
   813  func (r *topicStreamReaderImpl) onStopPartitionSessionRequest(m *rawtopicreader.StopPartitionSessionRequest) error {
   814  	session, err := r.sessionController.Get(m.PartitionSessionID)
   815  	if err != nil {
   816  		return err
   817  	}
   818  
   819  	if !m.Graceful {
   820  		session.Close()
   821  	}
   822  
   823  	return r.batcher.PushRawMessage(session, m)
   824  }