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