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

     1  package topicreaderinternal
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync/atomic"
     8  
     9  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    10  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader"
    11  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicreadercommon"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    14  )
    15  
    16  var errBatcherPopConcurency = xerrors.Wrap(errors.New("ydb: batch pop concurency, internal state error"))
    17  
    18  type batcher struct {
    19  	popInFlight    int64
    20  	closeErr       error
    21  	hasNewMessages empty.Chan
    22  
    23  	m xsync.Mutex
    24  
    25  	forceIgnoreMinRestrictionsOnNextMessagesBatch bool
    26  	closed                                        bool
    27  	closeChan                                     empty.Chan
    28  	messages                                      batcherMessagesMap
    29  }
    30  
    31  func newBatcher() *batcher {
    32  	return &batcher{
    33  		messages:       make(batcherMessagesMap),
    34  		closeChan:      make(empty.Chan),
    35  		hasNewMessages: make(empty.Chan, 1),
    36  	}
    37  }
    38  
    39  func (b *batcher) Close(err error) error {
    40  	b.m.Lock()
    41  	defer b.m.Unlock()
    42  
    43  	if b.closed {
    44  		return xerrors.WithStackTrace(fmt.Errorf("ydb: batch closed already: %w", err))
    45  	}
    46  
    47  	b.closed = true
    48  	b.closeErr = err
    49  	close(b.closeChan)
    50  
    51  	return nil
    52  }
    53  
    54  func (b *batcher) PushBatches(batches ...*topicreadercommon.PublicBatch) error {
    55  	b.m.Lock()
    56  	defer b.m.Unlock()
    57  	if b.closed {
    58  		return xerrors.WithStackTrace(fmt.Errorf("ydb: push batch to closed batcher :%w", b.closeErr))
    59  	}
    60  
    61  	for _, batch := range batches {
    62  		if err := b.addNeedLock(
    63  			topicreadercommon.GetCommitRange(batch).PartitionSession,
    64  			newBatcherItemBatch(batch),
    65  		); err != nil {
    66  			return err
    67  		}
    68  	}
    69  
    70  	return nil
    71  }
    72  
    73  func (b *batcher) PushRawMessage(session *topicreadercommon.PartitionSession, m rawtopicreader.ServerMessage) error {
    74  	b.m.Lock()
    75  	defer b.m.Unlock()
    76  
    77  	if b.closed {
    78  		return xerrors.WithStackTrace(fmt.Errorf("ydb: push raw message to closed batcher: %w", b.closeErr))
    79  	}
    80  
    81  	return b.addNeedLock(session, newBatcherItemRawMessage(m))
    82  }
    83  
    84  func (b *batcher) addNeedLock(session *topicreadercommon.PartitionSession, item batcherMessageOrderItem) error {
    85  	var currentItems batcherMessageOrderItems
    86  	var ok bool
    87  	var err error
    88  	if currentItems, ok = b.messages[session]; ok {
    89  		if currentItems, err = currentItems.Append(item); err != nil {
    90  			return err
    91  		}
    92  	} else {
    93  		currentItems = batcherMessageOrderItems{item}
    94  	}
    95  
    96  	b.messages[session] = currentItems
    97  
    98  	b.notifyAboutNewMessages()
    99  
   100  	return nil
   101  }
   102  
   103  type batcherGetOptions struct {
   104  	MinCount        int
   105  	MaxCount        int
   106  	rawMessagesOnly bool
   107  }
   108  
   109  func (o batcherGetOptions) cutBatchItemsHead(items batcherMessageOrderItems) (
   110  	head batcherMessageOrderItem,
   111  	rest batcherMessageOrderItems,
   112  	ok bool,
   113  ) {
   114  	notFound := func() (batcherMessageOrderItem, batcherMessageOrderItems, bool) {
   115  		return batcherMessageOrderItem{}, batcherMessageOrderItems{}, false
   116  	}
   117  	if len(items) == 0 {
   118  		return notFound()
   119  	}
   120  
   121  	if items[0].IsBatch() {
   122  		if o.rawMessagesOnly {
   123  			return notFound()
   124  		}
   125  
   126  		batchHead, batchRest, ok := o.splitBatch(items[0].Batch)
   127  
   128  		if !ok {
   129  			return notFound()
   130  		}
   131  
   132  		head = newBatcherItemBatch(batchHead)
   133  		rest = items.ReplaceHeadItem(newBatcherItemBatch(batchRest))
   134  
   135  		return head, rest, true
   136  	}
   137  
   138  	return items[0], items[1:], true
   139  }
   140  
   141  func (o batcherGetOptions) splitBatch(batch *topicreadercommon.PublicBatch) (
   142  	head, rest *topicreadercommon.PublicBatch,
   143  	ok bool,
   144  ) {
   145  	notFound := func() (*topicreadercommon.PublicBatch, *topicreadercommon.PublicBatch, bool) {
   146  		return nil, nil, false
   147  	}
   148  
   149  	if len(batch.Messages) < o.MinCount {
   150  		return notFound()
   151  	}
   152  
   153  	if o.MaxCount == 0 {
   154  		return batch, nil, true
   155  	}
   156  
   157  	head, rest = topicreadercommon.BatchCutMessages(batch, o.MaxCount)
   158  
   159  	return head, rest, true
   160  }
   161  
   162  func (b *batcher) Pop(ctx context.Context, opts batcherGetOptions) (_ batcherMessageOrderItem, err error) {
   163  	counter := atomic.AddInt64(&b.popInFlight, 1)
   164  	defer atomic.AddInt64(&b.popInFlight, -1)
   165  
   166  	if counter != 1 {
   167  		return batcherMessageOrderItem{}, xerrors.WithStackTrace(errBatcherPopConcurency)
   168  	}
   169  
   170  	if err = ctx.Err(); err != nil {
   171  		return batcherMessageOrderItem{}, err
   172  	}
   173  
   174  	for {
   175  		var findRes batcherResultCandidate
   176  		var closed bool
   177  
   178  		b.m.WithLock(func() {
   179  			closed = b.closed
   180  			if closed {
   181  				return
   182  			}
   183  
   184  			findRes = b.findNeedLock(opts)
   185  			if findRes.Ok {
   186  				b.applyNeedLock(&findRes)
   187  
   188  				return
   189  			}
   190  		})
   191  		if closed {
   192  			return batcherMessageOrderItem{},
   193  				xerrors.WithStackTrace(xerrors.Wrap(fmt.Errorf("ydb: try pop messages from closed batcher: %w", b.closeErr)))
   194  		}
   195  		if findRes.Ok {
   196  			return findRes.Result, nil
   197  		}
   198  
   199  		// wait new messages for next iteration
   200  		select {
   201  		case <-b.hasNewMessages:
   202  			// new iteration
   203  		case <-b.closeChan:
   204  			return batcherMessageOrderItem{},
   205  				xerrors.WithStackTrace(
   206  					fmt.Errorf(
   207  						"ydb: batcher close while pop wait new messages: %w",
   208  						b.closeErr,
   209  					),
   210  				)
   211  		case <-ctx.Done():
   212  			return batcherMessageOrderItem{}, ctx.Err()
   213  		}
   214  	}
   215  }
   216  
   217  func (b *batcher) notifyAboutNewMessages() {
   218  	select {
   219  	case b.hasNewMessages <- empty.Struct{}:
   220  		// sent signal
   221  	default:
   222  		// signal already in progress
   223  	}
   224  }
   225  
   226  type batcherResultCandidate struct {
   227  	Key         *topicreadercommon.PartitionSession
   228  	Result      batcherMessageOrderItem
   229  	Rest        batcherMessageOrderItems
   230  	WaiterIndex int
   231  	Ok          bool
   232  }
   233  
   234  func newBatcherResultCandidate(
   235  	key *topicreadercommon.PartitionSession,
   236  	result batcherMessageOrderItem,
   237  	rest batcherMessageOrderItems,
   238  	ok bool,
   239  ) batcherResultCandidate {
   240  	return batcherResultCandidate{
   241  		Key:    key,
   242  		Result: result,
   243  		Rest:   rest,
   244  		Ok:     ok,
   245  	}
   246  }
   247  
   248  func (b *batcher) findNeedLock(filter batcherGetOptions) batcherResultCandidate {
   249  	if len(b.messages) == 0 {
   250  		return batcherResultCandidate{}
   251  	}
   252  
   253  	rawMessageOpts := batcherGetOptions{rawMessagesOnly: true}
   254  
   255  	var batchResult batcherResultCandidate
   256  	needBatchResult := true
   257  
   258  	for k, items := range b.messages {
   259  		head, rest, ok := rawMessageOpts.cutBatchItemsHead(items)
   260  		if ok {
   261  			return newBatcherResultCandidate(k, head, rest, true)
   262  		}
   263  
   264  		if needBatchResult {
   265  			head, rest, ok = b.applyForceFlagToOptions(filter).cutBatchItemsHead(items)
   266  			if !ok {
   267  				continue
   268  			}
   269  
   270  			needBatchResult = false
   271  			batchResult = newBatcherResultCandidate(k, head, rest, true)
   272  		}
   273  	}
   274  
   275  	return batchResult
   276  }
   277  
   278  func (b *batcher) applyForceFlagToOptions(options batcherGetOptions) batcherGetOptions {
   279  	if !b.forceIgnoreMinRestrictionsOnNextMessagesBatch {
   280  		return options
   281  	}
   282  
   283  	res := options
   284  	res.MinCount = 1
   285  
   286  	return res
   287  }
   288  
   289  func (b *batcher) applyNeedLock(res *batcherResultCandidate) {
   290  	if res.Rest.IsEmpty() && res.WaiterIndex >= 0 {
   291  		delete(b.messages, res.Key)
   292  	} else {
   293  		b.messages[res.Key] = res.Rest
   294  	}
   295  
   296  	if res.Result.IsBatch() {
   297  		b.forceIgnoreMinRestrictionsOnNextMessagesBatch = false
   298  	}
   299  }
   300  
   301  func (b *batcher) IgnoreMinRestrictionsOnNextPop() {
   302  	b.m.Lock()
   303  	defer b.m.Unlock()
   304  
   305  	b.forceIgnoreMinRestrictionsOnNextMessagesBatch = true
   306  	b.notifyAboutNewMessages()
   307  }
   308  
   309  type batcherMessagesMap map[*topicreadercommon.PartitionSession]batcherMessageOrderItems
   310  
   311  type batcherMessageOrderItems []batcherMessageOrderItem
   312  
   313  func (items batcherMessageOrderItems) Append(item batcherMessageOrderItem) (batcherMessageOrderItems, error) {
   314  	if len(items) == 0 {
   315  		return append(items, item), nil
   316  	}
   317  
   318  	lastItem := &items[len(items)-1]
   319  	if item.IsBatch() && lastItem.IsBatch() {
   320  		if resBatch, err := topicreadercommon.BatchAppend(lastItem.Batch, item.Batch); err == nil {
   321  			lastItem.Batch = resBatch
   322  		} else {
   323  			return nil, err
   324  		}
   325  
   326  		return items, nil
   327  	}
   328  
   329  	return append(items, item), nil
   330  }
   331  
   332  func (items batcherMessageOrderItems) IsEmpty() bool {
   333  	return len(items) == 0
   334  }
   335  
   336  func (items batcherMessageOrderItems) ReplaceHeadItem(item batcherMessageOrderItem) batcherMessageOrderItems {
   337  	if item.IsEmpty() {
   338  		return items[1:]
   339  	}
   340  
   341  	res := make(batcherMessageOrderItems, len(items))
   342  	res[0] = item
   343  	copy(res[1:], items[1:])
   344  
   345  	return res
   346  }
   347  
   348  type batcherMessageOrderItem struct {
   349  	Batch      *topicreadercommon.PublicBatch
   350  	RawMessage rawtopicreader.ServerMessage
   351  }
   352  
   353  func newBatcherItemBatch(b *topicreadercommon.PublicBatch) batcherMessageOrderItem {
   354  	return batcherMessageOrderItem{Batch: b}
   355  }
   356  
   357  func newBatcherItemRawMessage(b rawtopicreader.ServerMessage) batcherMessageOrderItem {
   358  	return batcherMessageOrderItem{RawMessage: b}
   359  }
   360  
   361  func (item *batcherMessageOrderItem) IsBatch() bool {
   362  	return !topicreadercommon.BatchIsEmpty(item.Batch)
   363  }
   364  
   365  func (item *batcherMessageOrderItem) IsRawMessage() bool {
   366  	return item.RawMessage != nil
   367  }
   368  
   369  func (item *batcherMessageOrderItem) IsEmpty() bool {
   370  	return item.RawMessage == nil && topicreadercommon.BatchIsEmpty(item.Batch)
   371  }