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 }