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 }