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

     1  package topicreaderinternal
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  
     7  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
     8  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader"
     9  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    10  )
    11  
    12  var (
    13  	errBadSessionWhileMessageBatchCreate       = xerrors.Wrap(errors.New("ydb: bad session while messages batch create"))        //nolint:lll
    14  	errBadMessageOffsetWhileMessageBatchCreate = xerrors.Wrap(errors.New("ydb: bad message offset while messages batch create")) //nolint:lll
    15  )
    16  
    17  // PublicBatch is ordered group of message from one partition
    18  type PublicBatch struct {
    19  	empty.DoNotCopy
    20  
    21  	Messages []*PublicMessage
    22  
    23  	commitRange commitRange // от всех сообщений батча
    24  }
    25  
    26  func newBatch(session *partitionSession, messages []*PublicMessage) (*PublicBatch, error) {
    27  	for i := 0; i < len(messages); i++ {
    28  		msg := messages[i]
    29  
    30  		if msg.commitRange.partitionSession == nil {
    31  			msg.commitRange.partitionSession = session
    32  		}
    33  		if session != msg.commitRange.partitionSession {
    34  			return nil, xerrors.WithStackTrace(errBadSessionWhileMessageBatchCreate)
    35  		}
    36  
    37  		if i == 0 {
    38  			continue
    39  		}
    40  
    41  		prev := messages[i-1]
    42  		if prev.commitRange.commitOffsetEnd != msg.commitRange.commitOffsetStart {
    43  			return nil, xerrors.WithStackTrace(errBadMessageOffsetWhileMessageBatchCreate)
    44  		}
    45  	}
    46  
    47  	offset := commitRange{
    48  		partitionSession: session,
    49  	}
    50  	if len(messages) > 0 {
    51  		offset.commitOffsetStart = messages[0].commitRange.commitOffsetStart
    52  		offset.commitOffsetEnd = messages[len(messages)-1].commitRange.commitOffsetEnd
    53  	}
    54  
    55  	return &PublicBatch{
    56  		Messages:    messages,
    57  		commitRange: offset,
    58  	}, nil
    59  }
    60  
    61  func newBatchFromStream(
    62  	decoders decoderMap,
    63  	session *partitionSession,
    64  	sb rawtopicreader.Batch, //nolint:gocritic
    65  ) (*PublicBatch, error) {
    66  	messages := make([]*PublicMessage, len(sb.MessageData))
    67  	prevOffset := session.lastReceivedMessageOffset()
    68  	for i := range sb.MessageData {
    69  		sMess := &sb.MessageData[i]
    70  
    71  		dstMess := &PublicMessage{}
    72  		messages[i] = dstMess
    73  		dstMess.CreatedAt = sMess.CreatedAt
    74  		dstMess.MessageGroupID = sMess.MessageGroupID
    75  		dstMess.Offset = sMess.Offset.ToInt64()
    76  		dstMess.SeqNo = sMess.SeqNo
    77  		dstMess.WrittenAt = sb.WrittenAt
    78  		dstMess.ProducerID = sb.ProducerID
    79  		dstMess.WriteSessionMetadata = sb.WriteSessionMeta
    80  
    81  		dstMess.rawDataLen = len(sMess.Data)
    82  		dstMess.data = createReader(decoders, sb.Codec, sMess.Data)
    83  		dstMess.UncompressedSize = int(sMess.UncompressedSize)
    84  
    85  		dstMess.commitRange.partitionSession = session
    86  		dstMess.commitRange.commitOffsetStart = prevOffset + 1
    87  		dstMess.commitRange.commitOffsetEnd = sMess.Offset + 1
    88  
    89  		if len(sMess.MetadataItems) > 0 {
    90  			dstMess.Metadata = make(map[string][]byte, len(sMess.MetadataItems))
    91  			for metadataIndex := range sMess.MetadataItems {
    92  				dstMess.Metadata[sMess.MetadataItems[metadataIndex].Key] = sMess.MetadataItems[metadataIndex].Value
    93  			}
    94  		}
    95  
    96  		prevOffset = sMess.Offset
    97  	}
    98  
    99  	session.setLastReceivedMessageOffset(prevOffset)
   100  
   101  	return newBatch(session, messages)
   102  }
   103  
   104  // Context is cancelled when code should stop to process messages batch
   105  // for example - lost connection to server or receive stop partition signal without graceful flag
   106  func (m *PublicBatch) Context() context.Context {
   107  	return m.commitRange.partitionSession.Context()
   108  }
   109  
   110  // Topic is path of source topic of the messages in the batch
   111  func (m *PublicBatch) Topic() string {
   112  	return m.partitionSession().Topic
   113  }
   114  
   115  // PartitionID of messages in the batch
   116  func (m *PublicBatch) PartitionID() int64 {
   117  	return m.partitionSession().PartitionID
   118  }
   119  
   120  func (m *PublicBatch) partitionSession() *partitionSession {
   121  	return m.commitRange.partitionSession
   122  }
   123  
   124  func (m *PublicBatch) getCommitRange() PublicCommitRange {
   125  	return m.commitRange.getCommitRange()
   126  }
   127  
   128  func (m *PublicBatch) append(b *PublicBatch) (*PublicBatch, error) {
   129  	var res *PublicBatch
   130  	if m == nil {
   131  		res = &PublicBatch{}
   132  	} else {
   133  		res = m
   134  	}
   135  
   136  	if res.commitRange.partitionSession != b.commitRange.partitionSession {
   137  		return nil, xerrors.WithStackTrace(errors.New("ydb: bad partition session for merge"))
   138  	}
   139  
   140  	if res.commitRange.commitOffsetEnd != b.commitRange.commitOffsetStart {
   141  		return nil, xerrors.WithStackTrace(errors.New("ydb: bad offset interval for merge"))
   142  	}
   143  
   144  	res.Messages = append(res.Messages, b.Messages...)
   145  	res.commitRange.commitOffsetEnd = b.commitRange.commitOffsetEnd
   146  
   147  	return res, nil
   148  }
   149  
   150  func (m *PublicBatch) cutMessages(count int) (head, rest *PublicBatch) {
   151  	switch {
   152  	case count == 0:
   153  		return nil, m
   154  	case count >= len(m.Messages):
   155  		return m, nil
   156  	default:
   157  		// slice[0:count:count] - limit slice capacity and prevent overwrite rest by append messages to head
   158  		// explicit 0 need for prevent typos, when type slice[count:count] instead of slice[:count:count]
   159  		head, _ = newBatch(m.commitRange.partitionSession, m.Messages[0:count:count])
   160  		rest, _ = newBatch(m.commitRange.partitionSession, m.Messages[count:])
   161  
   162  		return head, rest
   163  	}
   164  }
   165  
   166  func (m *PublicBatch) isEmpty() bool {
   167  	return m == nil || len(m.Messages) == 0
   168  }
   169  
   170  func splitBytesByMessagesInBatches(batches []*PublicBatch, totalBytesCount int) error {
   171  	restBytes := totalBytesCount
   172  
   173  	cutBytes := func(want int) int {
   174  		switch {
   175  		case restBytes == 0:
   176  			return 0
   177  		case want >= restBytes:
   178  			res := restBytes
   179  			restBytes = 0
   180  
   181  			return res
   182  		default:
   183  			restBytes -= want
   184  
   185  			return want
   186  		}
   187  	}
   188  
   189  	messagesCount := 0
   190  	for batchIndex := range batches {
   191  		messagesCount += len(batches[batchIndex].Messages)
   192  		for messageIndex := range batches[batchIndex].Messages {
   193  			message := batches[batchIndex].Messages[messageIndex]
   194  			message.bufferBytesAccount = cutBytes(batches[batchIndex].Messages[messageIndex].rawDataLen)
   195  		}
   196  	}
   197  
   198  	if messagesCount == 0 {
   199  		if totalBytesCount == 0 {
   200  			return nil
   201  		}
   202  
   203  		return xerrors.NewWithIssues("ydb: can't split bytes to zero length messages count")
   204  	}
   205  
   206  	overheadPerMessage := restBytes / messagesCount
   207  	overheadRemind := restBytes % messagesCount
   208  
   209  	for batchIndex := range batches {
   210  		for messageIndex := range batches[batchIndex].Messages {
   211  			msg := batches[batchIndex].Messages[messageIndex]
   212  
   213  			msg.bufferBytesAccount += cutBytes(overheadPerMessage)
   214  			if overheadRemind > 0 {
   215  				msg.bufferBytesAccount += cutBytes(1)
   216  			}
   217  		}
   218  	}
   219  
   220  	return nil
   221  }