github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/topic/topicreaderinternal/batcher_test.go (about) 1 package topicreaderinternal 2 3 import ( 4 "context" 5 "errors" 6 "sync/atomic" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/ydb-platform/ydb-go-sdk/v3/internal/empty" 13 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon" 14 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader" 15 "github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicreadercommon" 16 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" 17 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest" 18 ) 19 20 func TestBatcher_PushBatch(t *testing.T) { 21 session1 := &topicreadercommon.PartitionSession{} 22 session2 := &topicreadercommon.PartitionSession{} 23 24 m11 := topicreadercommon.MessageWithSetCommitRangeForTest(&topicreadercommon.PublicMessage{ 25 WrittenAt: testTime(1), 26 }, topicreadercommon.CommitRange{PartitionSession: session1}) 27 28 m12 := topicreadercommon.MessageWithSetCommitRangeForTest(&topicreadercommon.PublicMessage{ 29 WrittenAt: testTime(2), 30 }, topicreadercommon.CommitRange{PartitionSession: session1}) 31 32 m21 := topicreadercommon.MessageWithSetCommitRangeForTest(&topicreadercommon.PublicMessage{ 33 WrittenAt: testTime(3), 34 }, topicreadercommon.CommitRange{PartitionSession: session2}) 35 36 m22 := topicreadercommon.MessageWithSetCommitRangeForTest(&topicreadercommon.PublicMessage{ 37 WrittenAt: testTime(4), 38 }, topicreadercommon.CommitRange{PartitionSession: session2}) 39 40 batch1 := mustNewBatch(session1, []*topicreadercommon.PublicMessage{m11, m12}) 41 batch2 := mustNewBatch(session2, []*topicreadercommon.PublicMessage{m21}) 42 batch3 := mustNewBatch(session2, []*topicreadercommon.PublicMessage{m22}) 43 44 b := newBatcher() 45 require.NoError(t, b.PushBatches(batch1)) 46 require.NoError(t, b.PushBatches(batch2)) 47 require.NoError(t, b.PushBatches(batch3)) 48 49 expectedSession1 := newBatcherItemBatch(mustNewBatch(session1, []*topicreadercommon.PublicMessage{m11, m12})) 50 expectedSession2 := newBatcherItemBatch(mustNewBatch(session2, []*topicreadercommon.PublicMessage{m21, m22})) 51 52 expected := batcherMessagesMap{ 53 session1: batcherMessageOrderItems{expectedSession1}, 54 session2: batcherMessageOrderItems{expectedSession2}, 55 } 56 require.Equal(t, expected, b.messages) 57 } 58 59 func TestBatcher_PushRawMessage(t *testing.T) { 60 t.Run("Empty", func(t *testing.T) { 61 b := newBatcher() 62 session := &topicreadercommon.PartitionSession{} 63 m := &rawtopicreader.StopPartitionSessionRequest{ 64 PartitionSessionID: 1, 65 } 66 require.NoError(t, b.PushRawMessage(session, m)) 67 68 expectedMap := batcherMessagesMap{session: batcherMessageOrderItems{newBatcherItemRawMessage(m)}} 69 require.Equal(t, expectedMap, b.messages) 70 }) 71 t.Run("AddRawAfterBatch", func(t *testing.T) { 72 b := newBatcher() 73 session := &topicreadercommon.PartitionSession{} 74 batch := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 75 m := &rawtopicreader.StopPartitionSessionRequest{ 76 PartitionSessionID: 1, 77 } 78 79 require.NoError(t, b.PushBatches(batch)) 80 require.NoError(t, b.PushRawMessage(session, m)) 81 82 expectedMap := batcherMessagesMap{session: batcherMessageOrderItems{ 83 newBatcherItemBatch(batch), 84 newBatcherItemRawMessage(m), 85 }} 86 require.Equal(t, expectedMap, b.messages) 87 }) 88 89 t.Run("AddBatchRawBatchBatch", func(t *testing.T) { 90 b := newBatcher() 91 session := &topicreadercommon.PartitionSession{} 92 batch1 := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 93 batch2 := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(2)}}) 94 batch3 := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(3)}}) 95 m := &rawtopicreader.StopPartitionSessionRequest{ 96 PartitionSessionID: 1, 97 } 98 99 require.NoError(t, b.PushBatches(batch1)) 100 require.NoError(t, b.PushRawMessage(session, m)) 101 require.NoError(t, b.PushBatches(batch2)) 102 require.NoError(t, b.PushBatches(batch3)) 103 104 expectedMap := batcherMessagesMap{session: batcherMessageOrderItems{ 105 newBatcherItemBatch(batch1), 106 newBatcherItemRawMessage(m), 107 newBatcherItemBatch(mustNewBatch( 108 session, 109 []*topicreadercommon.PublicMessage{{WrittenAt: testTime(2)}, {WrittenAt: testTime(3)}}, 110 )), 111 }} 112 require.Equal(t, expectedMap, b.messages) 113 }) 114 } 115 116 func TestBatcher_Pop(t *testing.T) { 117 t.Run("SimpleGet", func(t *testing.T) { 118 ctx := context.Background() 119 batch := mustNewBatch(nil, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 120 121 b := newBatcher() 122 require.NoError(t, b.PushBatches(batch)) 123 124 res, err := b.Pop(ctx, batcherGetOptions{}) 125 require.NoError(t, err) 126 require.Equal(t, newBatcherItemBatch(batch), res) 127 }) 128 129 t.Run("SimpleOneOfTwo", func(t *testing.T) { 130 ctx := context.Background() 131 session1 := &topicreadercommon.PartitionSession{} 132 session2 := &topicreadercommon.PartitionSession{} 133 134 mess1 := &topicreadercommon.PublicMessage{WrittenAt: testTime(1)} 135 topicreadercommon.MessageWithSetCommitRangeForTest(mess1, topicreadercommon.CommitRange{PartitionSession: session1}) 136 batch := mustNewBatch( 137 session1, 138 []*topicreadercommon.PublicMessage{mess1}, 139 ) 140 141 mess2 := &topicreadercommon.PublicMessage{WrittenAt: testTime(2)} 142 topicreadercommon.MessageWithSetCommitRangeForTest(mess2, topicreadercommon.CommitRange{PartitionSession: session2}) 143 batch2 := mustNewBatch( 144 session2, 145 []*topicreadercommon.PublicMessage{mess2}, 146 ) 147 148 b := newBatcher() 149 require.NoError(t, b.PushBatches(batch)) 150 require.NoError(t, b.PushBatches(batch2)) 151 152 possibleResults := []batcherMessageOrderItem{newBatcherItemBatch(batch), newBatcherItemBatch(batch2)} 153 154 res, err := b.Pop(ctx, batcherGetOptions{}) 155 require.NoError(t, err) 156 require.Contains(t, possibleResults, res) 157 require.Len(t, b.messages, 1) 158 159 res2, err := b.Pop(ctx, batcherGetOptions{}) 160 require.NoError(t, err) 161 require.Contains(t, possibleResults, res2) 162 require.NotEqual(t, res, res2) 163 require.Empty(t, b.messages) 164 }) 165 166 xtest.TestManyTimesWithName(t, "GetBeforePut", func(t testing.TB) { 167 ctx := context.Background() 168 batch := mustNewBatch(nil, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 169 170 b := newBatcher() 171 b.notifyAboutNewMessages() 172 173 go func() { 174 xtest.SpinWaitCondition(t, &b.m, func() bool { 175 return len(b.hasNewMessages) == 0 176 }) 177 _ = b.PushBatches(batch) 178 }() 179 180 res, err := b.Pop(ctx, batcherGetOptions{}) 181 require.NoError(t, err) 182 require.Equal(t, newBatcherItemBatch(batch), res) 183 require.Empty(t, b.messages) 184 }) 185 186 t.Run("GetMaxOne", func(t *testing.T) { 187 ctx := context.Background() 188 189 m1 := &topicreadercommon.PublicMessage{WrittenAt: testTime(1)} 190 m2 := &topicreadercommon.PublicMessage{WrittenAt: testTime(2)} 191 batch := mustNewBatch(nil, []*topicreadercommon.PublicMessage{m1, m2}) 192 193 b := newBatcher() 194 require.NoError(t, b.PushBatches(batch)) 195 196 res, err := b.Pop(ctx, batcherGetOptions{MaxCount: 1}) 197 require.NoError(t, err) 198 199 expectedResult := newBatcherItemBatch(mustNewBatch(nil, []*topicreadercommon.PublicMessage{m1})) 200 require.Equal(t, expectedResult, res) 201 202 expectedMessages := batcherMessagesMap{ 203 nil: batcherMessageOrderItems{newBatcherItemBatch(mustNewBatch(nil, []*topicreadercommon.PublicMessage{m2}))}, 204 } 205 require.Equal(t, expectedMessages, b.messages) 206 }) 207 208 t.Run("GetFirstMessageFromSameSession", func(t *testing.T) { 209 b := newBatcher() 210 batch := mustNewBatch(nil, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 211 require.NoError(t, b.PushBatches(batch)) 212 require.NoError(t, b.PushRawMessage(nil, &rawtopicreader.StopPartitionSessionRequest{PartitionSessionID: 1})) 213 214 res, err := b.Pop(context.Background(), batcherGetOptions{}) 215 require.NoError(t, err) 216 require.Equal(t, newBatcherItemBatch(batch), res) 217 }) 218 219 t.Run("PreferFirstRawMessageFromDifferentSessions", func(t *testing.T) { 220 session1 := &topicreadercommon.PartitionSession{} 221 session2 := &topicreadercommon.PartitionSession{} 222 223 b := newBatcher() 224 m := &rawtopicreader.StopPartitionSessionRequest{PartitionSessionID: 1} 225 226 require.NoError(t, b.PushBatches(mustNewBatch( 227 session1, 228 []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}, 229 ))) 230 require.NoError(t, b.PushRawMessage(session2, m)) 231 232 res, err := b.Pop(context.Background(), batcherGetOptions{}) 233 require.NoError(t, err) 234 require.Equal(t, newBatcherItemRawMessage(m), res) 235 }) 236 237 xtest.TestManyTimesWithName(t, "CloseBatcherWhilePopWait", func(t testing.TB) { 238 ctx := xtest.Context(t) 239 testErr := errors.New("test") 240 241 b := newBatcher() 242 b.notifyAboutNewMessages() 243 244 require.Len(t, b.hasNewMessages, 1) 245 246 popFinished := make(empty.Chan) 247 popGoroutineStarted := make(empty.Chan) 248 go func() { 249 close(popGoroutineStarted) 250 251 _, popErr := b.Pop(ctx, batcherGetOptions{MinCount: 1}) 252 require.ErrorIs(t, popErr, testErr) 253 close(popFinished) 254 }() 255 256 xtest.WaitChannelClosed(t, popGoroutineStarted) 257 258 // loop for wait Pop start wait message 259 xtest.SpinWaitCondition(t, &b.m, func() bool { 260 return len(b.hasNewMessages) == 0 261 }) 262 263 require.NoError(t, b.Close(testErr)) 264 require.Error(t, b.Close(errors.New("second close"))) 265 266 xtest.WaitChannelClosed(t, popFinished) 267 }) 268 } 269 270 func TestBatcher_PopMinIgnored(t *testing.T) { 271 t.Run("PopAfterForce", func(t *testing.T) { 272 b := newBatcher() 273 require.NoError(t, b.PushBatches(&topicreadercommon.PublicBatch{Messages: []*topicreadercommon.PublicMessage{ 274 { 275 SeqNo: 1, 276 }, 277 }})) 278 279 b.IgnoreMinRestrictionsOnNextPop() 280 281 ctx, cancel := xcontext.WithTimeout(context.Background(), time.Second) 282 defer cancel() 283 284 batch, err := b.Pop(ctx, batcherGetOptions{MinCount: 2}) 285 require.NoError(t, err) 286 require.Len(t, batch.Batch.Messages, 1) 287 require.False(t, b.forceIgnoreMinRestrictionsOnNextMessagesBatch) 288 }) 289 290 xtest.TestManyTimesWithName(t, "ForceAfterPop", func(t testing.TB) { 291 b := newBatcher() 292 require.NoError(t, b.PushBatches(&topicreadercommon.PublicBatch{Messages: []*topicreadercommon.PublicMessage{ 293 { 294 SeqNo: 1, 295 }, 296 }})) 297 298 var IgnoreMinRestrictionsOnNextPopDone atomic.Int64 299 go func() { 300 defer IgnoreMinRestrictionsOnNextPopDone.Add(1) 301 302 xtest.SpinWaitCondition(t, &b.m, func() bool { 303 return len(b.hasNewMessages) == 0 304 }) 305 b.IgnoreMinRestrictionsOnNextPop() 306 }() 307 308 ctx, cancel := xcontext.WithTimeout(context.Background(), time.Second) 309 defer cancel() 310 311 batch, err := b.Pop(ctx, batcherGetOptions{MinCount: 2}) 312 313 require.NoError(t, err, IgnoreMinRestrictionsOnNextPopDone.Load()) 314 require.Len(t, batch.Batch.Messages, 1) 315 require.False(t, b.forceIgnoreMinRestrictionsOnNextMessagesBatch) 316 }) 317 } 318 319 func TestBatcherConcurency(t *testing.T) { 320 xtest.TestManyTimesWithName(t, "OneBatch", func(tb testing.TB) { 321 b := newBatcher() 322 323 go func() { 324 _ = b.PushBatches(&topicreadercommon.PublicBatch{Messages: []*topicreadercommon.PublicMessage{{SeqNo: 1}}}) 325 }() 326 327 ctx, cancel := xcontext.WithTimeout(context.Background(), time.Second) 328 defer cancel() 329 330 batch, err := b.Pop(ctx, batcherGetOptions{MinCount: 1}) 331 require.NoError(tb, err) 332 require.Equal(tb, int64(1), batch.Batch.Messages[0].SeqNo) 333 }) 334 335 xtest.TestManyTimesWithName(t, "ManyRawMessages", func(tb testing.TB) { 336 const count = 10 337 b := newBatcher() 338 session := &topicreadercommon.PartitionSession{} 339 340 go func() { 341 for i := 0; i < count; i++ { 342 _ = b.PushRawMessage(session, &rawtopicreader.StartPartitionSessionRequest{ 343 CommittedOffset: rawtopiccommon.NewOffset(int64(i)), 344 PartitionOffsets: rawtopiccommon.OffsetRange{}, 345 }) 346 } 347 }() 348 349 ctx, cancel := xcontext.WithTimeout(context.Background(), time.Second) 350 defer cancel() 351 352 for i := 0; i < count; i++ { 353 res, err := b.Pop(ctx, batcherGetOptions{MinCount: 1}) 354 require.NoError(tb, err) 355 require.Equal( 356 tb, 357 rawtopiccommon.NewOffset(int64(i)), 358 res.RawMessage.(*rawtopicreader.StartPartitionSessionRequest).CommittedOffset, 359 ) 360 } 361 }) 362 } 363 364 func TestBatcher_Find(t *testing.T) { 365 t.Run("Empty", func(t *testing.T) { 366 b := newBatcher() 367 findRes := b.findNeedLock(batcherGetOptions{}) 368 require.False(t, findRes.Ok) 369 }) 370 t.Run("FoundEmptyFilter", func(t *testing.T) { 371 session := &topicreadercommon.PartitionSession{} 372 batch := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 373 374 b := newBatcher() 375 376 require.NoError(t, b.PushBatches(batch)) 377 378 findRes := b.findNeedLock(batcherGetOptions{}) 379 expectedResult := batcherResultCandidate{ 380 Key: session, 381 Result: newBatcherItemBatch(batch), 382 Rest: batcherMessageOrderItems{}, 383 WaiterIndex: 0, 384 Ok: true, 385 } 386 require.Equal(t, expectedResult, findRes) 387 }) 388 389 t.Run("FoundPartialBatchFilter", func(t *testing.T) { 390 session := &topicreadercommon.PartitionSession{} 391 batch := mustNewBatch( 392 session, 393 []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}, {WrittenAt: testTime(2)}}, 394 ) 395 396 b := newBatcher() 397 398 require.NoError(t, b.PushBatches(batch)) 399 400 findRes := b.findNeedLock(batcherGetOptions{MaxCount: 1}) 401 402 expectedResult := newBatcherItemBatch(mustNewBatch( 403 session, 404 []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}, 405 )) 406 expectedRestBatch := newBatcherItemBatch(mustNewBatch( 407 session, 408 []*topicreadercommon.PublicMessage{{WrittenAt: testTime(2)}}, 409 )) 410 411 expectedCandidate := batcherResultCandidate{ 412 Key: session, 413 Result: expectedResult, 414 Rest: batcherMessageOrderItems{expectedRestBatch}, 415 WaiterIndex: 0, 416 Ok: true, 417 } 418 require.Equal(t, expectedCandidate, findRes) 419 }) 420 } 421 422 func TestBatcher_Apply(t *testing.T) { 423 t.Run("New", func(t *testing.T) { 424 session := &topicreadercommon.PartitionSession{} 425 b := newBatcher() 426 427 batch := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 428 foundRes := batcherResultCandidate{ 429 Key: session, 430 Rest: batcherMessageOrderItems{newBatcherItemBatch(batch)}, 431 } 432 b.applyNeedLock(&foundRes) 433 434 expectedMap := batcherMessagesMap{session: batcherMessageOrderItems{newBatcherItemBatch(batch)}} 435 require.Equal(t, expectedMap, b.messages) 436 }) 437 438 t.Run("Delete", func(t *testing.T) { 439 session := &topicreadercommon.PartitionSession{} 440 b := newBatcher() 441 442 batch := mustNewBatch(session, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 443 444 foundRes := batcherResultCandidate{ 445 Key: session, 446 Rest: batcherMessageOrderItems{}, 447 } 448 449 b.messages = batcherMessagesMap{session: batcherMessageOrderItems{newBatcherItemBatch(batch)}} 450 451 b.applyNeedLock(&foundRes) 452 453 require.Empty(t, b.messages) 454 }) 455 } 456 457 func TestBatcherGetOptions_Split(t *testing.T) { 458 t.Run("Empty", func(t *testing.T) { 459 opts := batcherGetOptions{} 460 batch := mustNewBatch(nil, []*topicreadercommon.PublicMessage{ 461 {WrittenAt: testTime(1)}, 462 {WrittenAt: testTime(2)}, 463 }, 464 ) 465 head, rest, ok := opts.splitBatch(batch) 466 467 require.Equal(t, batch, head) 468 require.True(t, topicreadercommon.BatchIsEmpty(rest)) 469 require.True(t, ok) 470 }) 471 t.Run("MinCount", func(t *testing.T) { 472 opts := batcherGetOptions{MinCount: 2} 473 batch1 := mustNewBatch(nil, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 474 batch2 := mustNewBatch(nil, []*topicreadercommon.PublicMessage{ 475 {WrittenAt: testTime(1)}, 476 {WrittenAt: testTime(2)}, 477 }, 478 ) 479 480 head, rest, ok := opts.splitBatch(batch1) 481 require.True(t, topicreadercommon.BatchIsEmpty(head)) 482 require.True(t, topicreadercommon.BatchIsEmpty(rest)) 483 require.False(t, ok) 484 485 head, rest, ok = opts.splitBatch(batch2) 486 require.Equal(t, batch2, head) 487 require.True(t, topicreadercommon.BatchIsEmpty(rest)) 488 require.True(t, ok) 489 }) 490 t.Run("MaxCount", func(t *testing.T) { 491 opts := batcherGetOptions{MaxCount: 2} 492 batch1 := mustNewBatch(nil, []*topicreadercommon.PublicMessage{{WrittenAt: testTime(1)}}) 493 batch2 := mustNewBatch(nil, []*topicreadercommon.PublicMessage{ 494 {WrittenAt: testTime(1)}, 495 {WrittenAt: testTime(2)}, 496 }, 497 ) 498 batch3 := mustNewBatch( 499 nil, 500 []*topicreadercommon.PublicMessage{ 501 {WrittenAt: testTime(11)}, 502 {WrittenAt: testTime(12)}, 503 {WrittenAt: testTime(13)}, 504 {WrittenAt: testTime(14)}, 505 }, 506 ) 507 508 head, rest, ok := opts.splitBatch(batch1) 509 require.Equal(t, batch1, head) 510 require.True(t, topicreadercommon.BatchIsEmpty(rest)) 511 require.True(t, ok) 512 513 head, rest, ok = opts.splitBatch(batch2) 514 require.Equal(t, batch2, head) 515 require.True(t, topicreadercommon.BatchIsEmpty(rest)) 516 require.True(t, ok) 517 518 head, rest, ok = opts.splitBatch(batch3) 519 expectedHead := mustNewBatch( 520 nil, 521 []*topicreadercommon.PublicMessage{ 522 {WrittenAt: testTime(11)}, 523 {WrittenAt: testTime(12)}, 524 }, 525 ) 526 expectedRest := mustNewBatch( 527 nil, 528 []*topicreadercommon.PublicMessage{ 529 {WrittenAt: testTime(13)}, 530 {WrittenAt: testTime(14)}, 531 }, 532 ) 533 require.Equal(t, expectedHead, head) 534 require.Equal(t, expectedRest, rest) 535 require.True(t, ok) 536 }) 537 } 538 539 func TestBatcher_Fire(t *testing.T) { 540 t.Run("Empty", func(t *testing.T) { 541 b := newBatcher() 542 b.notifyAboutNewMessages() 543 }) 544 } 545 546 func mustNewBatch( 547 session *topicreadercommon.PartitionSession, 548 messages []*topicreadercommon.PublicMessage, 549 ) *topicreadercommon.PublicBatch { 550 return xtest.Must(topicreadercommon.NewBatch(session, messages)) 551 } 552 553 func testTime(num int) time.Time { 554 return time.Date(2022, 6, 17, 0, 0, 0, num, time.UTC) 555 }