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