/
githubmirror
/
ydb-go-sdk
Обзор
Документация
Войти
/
githubmirror
/
ydb-go-sdk
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
internal/topic/topicwriterinternal/queue_test.go
516 строк
12 KB
Kuzin Roman
LOGBROKER-10206 Refactor writers (#2067)
31 мар 2026, 12:29
Не верифицирован
31 мар 2026, 12:29
396ab29
Код
Авторство
О чём код?
package topicwriterinternal import ( "context" "errors" "math/rand" "runtime" "runtime/debug" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/ydb-platform/ydb-go-sdk/v3/internal/empty" "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicwriter" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" ) func TestMessageQueue_AddMessages(t *testing.T) { t.Run("Empty", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 3, 5))) require.Equal(t, 3, q.lastWrittenIndex) expected := map[int]messageWithDataContent{ 1: newTestMessageWithDataContent(1), 2: newTestMessageWithDataContent(3), 3: newTestMessageWithDataContent(5), } require.Equal(t, expected, q.messagesByOrder) require.Len(t, q.seqNoToOrderID, 3) require.Equal(t, 1, q.seqNoToOrderID[1]) require.Equal(t, 2, q.seqNoToOrderID[3]) require.Equal(t, 3, q.seqNoToOrderID[5]) }) t.Run("Closed", func(t *testing.T) { q := newMessageQueue() _ = q.Close(errors.New("err")) require.Error(t, q.AddMessages(newTestMessagesWithContent(1, 3, 5))) }) t.Run("OverflowIndex", func(t *testing.T) { q := newMessageQueue() q.lastWrittenIndex = maxInt - 1 require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 3, 5))) require.Len(t, q.messagesByOrder, 3) q.messagesByOrder[maxInt] = newTestMessageWithDataContent(1) q.messagesByOrder[minInt] = newTestMessageWithDataContent(3) q.messagesByOrder[minInt+1] = newTestMessageWithDataContent(5) require.Equal(t, minInt+1, q.lastWrittenIndex) }) t.Run("BadOrder", func(t *testing.T) { q := newMessageQueue() require.Error(t, q.AddMessages(newTestMessagesWithContent(2, 1))) }) } func TestMessageQueue_CheckMessages(t *testing.T) { t.Run("Empty", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent())) }) t.Run("Unordered", func(t *testing.T) { q := newMessageQueue() require.Error(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent(2, 2))) require.Error(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent(2, 1))) }) t.Run("NoGreaterThenLastSent", func(t *testing.T) { q := newMessageQueue() q.lastSeqNo = 10 require.Error(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent(int(q.lastSeqNo-1)))) require.Error(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent(int(q.lastSeqNo)))) require.NoError(t, q.checkNewMessagesBeforeAddNeedLock(newTestMessagesWithContent(int(q.lastSeqNo+1)))) }) } func TestMessageQueue_Close(t *testing.T) { q := newMessageQueue() testErr := errors.New("test") require.NoError(t, q.Close(testErr)) require.Error(t, q.Close(errors.New("second"))) require.Equal(t, testErr, q.closedErr) require.True(t, q.closed) <-q.closedChan } func TestMessageQueue_GetMessages(t *testing.T) { ctx := context.Background() t.Run("Simple", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2))) require.NoError(t, q.AddMessages(newTestMessagesWithContent(3, 4))) messages, err := q.GetMessagesForSend(ctx) require.NoError(t, err) require.Equal(t, []int64{1, 2, 3, 4}, getSeqNumbers(messages)) }) t.Run("SendMessagesAfterStartWait", func(t *testing.T) { q := newMessageQueue() var err error var messages []messageWithDataContent gotMessages := make(empty.Chan) go func() { messages, err = q.GetMessagesForSend(ctx) close(gotMessages) }() waitGetMessageStarted(&q) require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2, 3))) <-gotMessages require.NoError(t, err) require.Equal(t, []int64{1, 2, 3}, getSeqNumbers(messages)) }) t.Run("Stress", func(t *testing.T) { iterations := 100000 q := newMessageQueue() var lastSentSeqNo int64 sendFinished := make(empty.Chan) fatalChan := make(chan string) go func() { //nolint:gosec sendRand := rand.New(rand.NewSource(0)) for range iterations { count := sendRand.Intn(10) + 1 m := make([]messageWithDataContent, 0, count) for range count { number := int(atomic.AddInt64(&lastSentSeqNo, 1)) m = append(m, newTestMessageWithDataContent(number)) } require.NoError(t, q.AddMessages(m)) } close(sendFinished) }() readFinished := make(empty.Chan) var lastReadSeqNo atomic.Int64 readCtx, readCancel := xcontext.WithCancel(ctx) defer readCancel() go func() { defer close(readFinished) for { messages, err := q.GetMessagesForSend(readCtx) if err != nil { break } for _, mess := range messages { if lastReadSeqNo.Load()+1 != mess.SeqNo { fatalChan <- string(debug.Stack()) return } lastReadSeqNo.Store(mess.SeqNo) } } }() select { case <-sendFinished: case stack := <-fatalChan: t.Fatal(stack) } waitTimeout := time.Second * 10 startWait := time.Now() waitReader: for { if lastReadSeqNo.Load() == lastSentSeqNo { readCancel() } select { case <-readFinished: break waitReader case stack := <-fatalChan: t.Fatal(stack) default: } runtime.Gosched() if time.Since(startWait) > waitTimeout { t.Fatal() } } }) t.Run("ClosedContext", func(t *testing.T) { closedCtx, cancel := xcontext.WithCancel(ctx) cancel() q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2))) _, err := q.GetMessagesForSend(closedCtx) require.ErrorIs(t, err, context.Canceled) }) t.Run("CallOnClosedQueue", func(t *testing.T) { q := newMessageQueue() _ = q.Close(errors.New("test")) _, err := q.GetMessagesForSend(ctx) require.Error(t, err) }) t.Run("CloseContextAfterCall", func(t *testing.T) { q := newMessageQueue() q.notifyNewMessages() var err error gotErr := make(empty.Chan) go func() { _, err = q.GetMessagesForSend(ctx) close(gotErr) }() waitGetMessageStarted(&q) testErr := errors.New("test") require.NoError(t, q.Close(testErr)) <-gotErr require.ErrorIs(t, err, testErr) }) } func TestMessageQueue_ResetSentProgress(t *testing.T) { ctx := context.Background() t.Run("Simple", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2, 3))) res1, err := q.GetMessagesForSend(ctx) require.NoError(t, err) q.ResetSentProgress() require.Equal(t, 0, q.lastSentIndex) require.Equal(t, 3, q.lastWrittenIndex) res2, err := q.GetMessagesForSend(ctx) require.NoError(t, err) require.Equal(t, res1, res2) }) t.Run("Overflow", func(t *testing.T) { q := newMessageQueue() q.lastWrittenIndex = maxInt - 1 q.lastSentIndex = q.lastWrittenIndex require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2, 3))) res1, err := q.GetMessagesForSend(ctx) require.NoError(t, err) q.ResetSentProgress() require.Equal(t, maxInt-1, q.lastSentIndex) require.Equal(t, minInt+1, q.lastWrittenIndex) res2, err := q.GetMessagesForSend(ctx) require.NoError(t, err) require.Equal(t, res1, res2) }) } func TestIsFirstCycledIndexLess(t *testing.T) { table := []struct { name string first int second int result bool }{ { name: "smallPositivesFirstLess", first: 1, second: 2, result: true, }, { name: "smallPositivesEquals", first: 1, second: 1, result: false, }, { name: "smallPositivesFirstGreater", first: 2, second: 1, result: false, }, { name: "edgePositivesFirstLess", first: minPositiveIndexWhichOrderLessThenNegative - 1, second: minPositiveIndexWhichOrderLessThenNegative, result: true, }, { name: "edgePositivesFirstGreater", first: minPositiveIndexWhichOrderLessThenNegative, second: minPositiveIndexWhichOrderLessThenNegative - 1, result: false, }, { name: "overflowEdgeFirstPositive", first: maxInt, second: minInt, result: true, }, { name: "overflowEdgeFirstNegative", first: minInt, second: maxInt, result: false, }, { name: "nearZeroFirstNegativeSecondZero", first: -1, second: 0, result: true, }, { name: "nearZeroFirstZeroSecondNegative", first: 0, second: -1, result: false, }, { name: "nearZeroFirstZeroSecondPositive", first: 0, second: 1, result: true, }, { name: "nearZeroFirstNegativeSecondPositive", first: -1, second: 1, result: true, }, { name: "nearZeroFirstPositiveSecondNegative", first: 1, second: -1, result: false, }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { require.Equal(t, test.result, isFirstCycledIndexLess(test.first, test.second)) }) } } func TestMinMaxIntConst(t *testing.T) { v := maxInt v++ require.Equal(t, minInt, v) } func TestSortIndexes(t *testing.T) { table := []struct { name string source []int expected []int }{ { name: "empty", source: []int{}, expected: []int{}, }, { name: "usual", source: []int{30, 1, 2}, expected: []int{1, 2, 30}, }, { name: "nearZero", source: []int{0, 1, -1}, expected: []int{-1, 0, 1}, }, { name: "indexoverflow", source: []int{minInt, minInt + 1, maxInt - 1, maxInt}, expected: []int{maxInt - 1, maxInt, minInt, minInt + 1}, }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { sortMessageQueueIndexes(test.source) require.Equal(t, test.expected, test.source) }) } } func TestQueuePanicOnOverflow(t *testing.T) { require.Panics(t, func() { q := newMessageQueue() q.messagesByOrder[123] = messageWithDataContent{} q.lastWrittenIndex = maxInt q.addMessageNeedLock(messageWithDataContent{}) }) } func TestRegressionIssue1038_ReceiveAckAfterCloseQueue(t *testing.T) { counter := 0 q := newMessageQueue() q.OnAckReceived = func(count int) { counter -= count } require.NoError(t, q.AddMessages(newTestMessagesWithContent(1))) counter++ require.NoError(t, q.Close(errors.New("test err"))) require.ErrorIs(t, q.AcksReceived([]rawtopicwriter.WriteAck{ { SeqNo: 1, MessageWriteStatus: rawtopicwriter.MessageWriteStatus{}, }, }), errAckOnClosedMessageQueue) require.Zero(t, counter) } func TestQueue_Ack(t *testing.T) { t.Run("First", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1, 2, 5))) require.NoError(t, q.AcksReceived([]rawtopicwriter.WriteAck{ { SeqNo: 2, }, })) expectedMap := map[int]messageWithDataContent{ 1: newTestMessageWithDataContent(1), 3: newTestMessageWithDataContent(5), } require.Equal(t, expectedMap, q.messagesByOrder) }) t.Run("Unexisted", func(t *testing.T) { q := newMessageQueue() require.NoError(t, q.AddMessages(newTestMessagesWithContent(1))) // remove first with the seqno require.Error(t, q.AcksReceived([]rawtopicwriter.WriteAck{ { SeqNo: 5, }, })) expectedMap := map[int]messageWithDataContent{ 1: newTestMessageWithDataContent(1), } require.Equal(t, expectedMap, q.messagesByOrder) }) t.Run("OnAckReceived", func(t *testing.T) { receivedCount := 0 q := newMessageQueue() q.OnAckReceived = func(count int) { receivedCount = count } err := q.AddMessages(newTestMessagesWithContent(1, 2, 3)) require.NoError(t, err) err = q.AcksReceived([]rawtopicwriter.WriteAck{ { SeqNo: 1, }, { SeqNo: 3, }, }) require.NoError(t, err) require.Equal(t, 2, receivedCount) // Double ack err = q.AcksReceived([]rawtopicwriter.WriteAck{ { SeqNo: 1, }, { SeqNo: 3, }, }) require.Error(t, err) require.Equal(t, 0, receivedCount) }) } func waitGetMessageStarted(q *messageQueue) { q.notifyNewMessages() for len(q.hasNewMessages) != 0 { runtime.Gosched() } } func getSeqNumbers(messages []messageWithDataContent) []int64 { res := make([]int64, 0, len(messages)) for i := range messages { res = append(res, messages[i].SeqNo) } return res }