/
githubmirror
/
ydb-go-sdk
Обзор
Документация
Войти
/
githubmirror
/
ydb-go-sdk
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
internal/topic/topicreaderinternal/stream_reader_impl_test.go
1 444 строки
42 KB
Konstantin Prokopenko
update offset bugfix (#1968)
11 дек 2025, 20:45
Не верифицирован
11 дек 2025, 20:45
8433105
Код
Авторство
О чём код?
package topicreaderinternal import ( "bytes" "compress/gzip" "context" "errors" "io" "sync" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/ydb-platform/ydb-go-sdk/v3/internal/empty" "github.com/ydb-platform/ydb-go-sdk/v3/internal/endpoint" "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic" "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon" "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader" "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawydb" "github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicreadercommon" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync" xtest "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xtest" "github.com/ydb-platform/ydb-go-sdk/v3/trace" ) func TestTopicStreamReaderImpl_BufferCounterOnStopPartition(t *testing.T) { table := []struct { name string graceful bool }{ { name: "graceful", graceful: true, }, { name: "force", graceful: false, }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { e := newTopicReaderTestEnv(t) e.Start() initialBufferSize := e.reader.restBufferSizeBytes.Load() messageSize := initialBufferSize - 1 e.stream.EXPECT().Send(&rawtopicreader.ReadRequest{BytesSize: int(messageSize)}).MaxTimes(1) messageReaded := make(empty.Chan) e.SendFromServerAndSetNextCallback(&rawtopicreader.ReadResponse{ BytesSize: int(messageSize), PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: 0, ProducerID: "", WriteSessionMeta: nil, WrittenAt: time.Time{}, MessageData: []rawtopicreader.MessageData{ { Offset: 1, SeqNo: 1, }, }, }, }, }, }, }, func() { close(messageReaded) }) <-messageReaded require.Equal(t, int64(1), e.reader.restBufferSizeBytes.Load()) partitionStopped := make(empty.Chan) e.SendFromServerAndSetNextCallback(&rawtopicreader.StopPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{}, PartitionSessionID: e.partitionSessionID, Graceful: test.graceful, CommittedOffset: 0, }, func() { close(partitionStopped) }) <-partitionStopped fixedBufferSizeCtx, cancel := context.WithCancel(e.ctx) go func() { xtest.SpinWaitCondition(t, nil, func() bool { return initialBufferSize == e.reader.restBufferSizeBytes.Load() }) cancel() }() _, _ = e.reader.ReadMessageBatch(fixedBufferSizeCtx, newReadMessageBatchOptions()) <-fixedBufferSizeCtx.Done() require.Equal(t, initialBufferSize, e.reader.restBufferSizeBytes.Load()) }) } } func TestTopicStreamReaderImpl_CommitStolen(t *testing.T) { xtest.TestManyTimesWithName(t, "SimpleCommit", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() lastOffset := e.partitionSession.LastReceivedMessageOffset() const dataSize = 4 // request new data portion readRequestReceived := make(empty.Chan) e.stream.EXPECT().Send( &rawtopicreader.ReadRequest{BytesSize: dataSize * 2}, ).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(readRequestReceived) return nil }) commitReceived := make(empty.Chan) // Expect commit message with stole e.stream.EXPECT().Send( &rawtopicreader.CommitOffsetRequest{ CommitOffsets: []rawtopicreader.PartitionCommitOffset{ { PartitionSessionID: e.partitionSessionID, Offsets: []rawtopiccommon.OffsetRange{ { Start: lastOffset + 1, End: lastOffset + 16, }, }, }, }, }, ).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(commitReceived) return nil }) // send message with stole offsets // e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, ProducerID: "1", MessageData: []rawtopicreader.MessageData{ { Offset: lastOffset + 10, }, }, }, }, }, }, }) e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, ProducerID: "1", MessageData: []rawtopicreader.MessageData{ { Offset: lastOffset + 15, }, }, }, }, }, }, }) opts := newReadMessageBatchOptions() opts.MinCount = 2 batch, err := e.reader.ReadMessageBatch(e.ctx, opts) require.NoError(t, err) require.NoError(t, e.reader.Commit(e.ctx, topicreadercommon.GetCommitRange(batch))) xtest.WaitChannelClosed(t, commitReceived) xtest.WaitChannelClosed(t, readRequestReceived) }) xtest.TestManyTimesWithName(t, "WrongOrderCommitWithSyncMode", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.reader.cfg.CommitMode = topicreadercommon.CommitModeSync e.Start() lastOffset := e.partitionSession.LastReceivedMessageOffset() const dataSize = 4 // request new data portion readRequestReceived := make(empty.Chan) e.stream.EXPECT().Send( &rawtopicreader.ReadRequest{BytesSize: dataSize * 2}, ).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(readRequestReceived) return nil }) e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, ProducerID: "1", MessageData: []rawtopicreader.MessageData{ { Offset: lastOffset + 1, }, }, }, }, }, }, }) e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, ProducerID: "1", MessageData: []rawtopicreader.MessageData{ { Offset: lastOffset + 2, }, }, }, }, }, }, }) opts := newReadMessageBatchOptions() opts.MinCount = 2 batch, err := e.reader.ReadMessageBatch(e.ctx, opts) require.NoError(t, err) require.ErrorIs(t, e.reader.Commit( e.ctx, topicreadercommon.GetCommitRange(batch.Messages[1]), ), topicreadercommon.ErrWrongCommitOrderInSyncMode) xtest.WaitChannelClosed(t, readRequestReceived) }) xtest.TestManyTimesWithName(t, "CommitAfterGracefulStopPartition", func(t testing.TB) { e := newTopicReaderTestEnv(t) committed := e.partitionSession.CommittedOffset() commitReceived := make(empty.Chan) e.stream.EXPECT().Send(&rawtopicreader.CommitOffsetRequest{CommitOffsets: []rawtopicreader.PartitionCommitOffset{ { PartitionSessionID: e.partitionSessionID, Offsets: []rawtopiccommon.OffsetRange{ { Start: committed, End: committed + 1, }, }, }, }}).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(commitReceived) return nil }) stopPartitionResponseSent := make(empty.Chan) e.stream.EXPECT().Send(&rawtopicreader.StopPartitionSessionResponse{PartitionSessionID: e.partitionSessionID}). DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(stopPartitionResponseSent) return nil }) e.Start() // send from server message, then partition graceful stop request go func() { e.SendFromServer(&rawtopicreader.ReadResponse{ PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{ { Offset: committed, SeqNo: 1, }, }, }, }, }, }, }) e.SendFromServer(&rawtopicreader.StopPartitionSessionRequest{ PartitionSessionID: e.partitionSessionID, Graceful: true, }) }() readCtx, readCtxCancel := xcontext.WithCancel(e.ctx) go func() { <-stopPartitionResponseSent readCtxCancel() }() batch, err := e.reader.ReadMessageBatch(readCtx, newReadMessageBatchOptions()) require.NoError(t, err) err = e.reader.Commit(e.ctx, topicreadercommon.GetCommitRange(batch)) require.NoError(t, err) _, err = e.reader.ReadMessageBatch(readCtx, newReadMessageBatchOptions()) require.ErrorIs(t, err, context.Canceled) select { case <-e.partitionSession.Context().Done(): // pass case <-time.After(time.Second): t.Fatal("partition session not closed") } xtest.WaitChannelClosed(t, commitReceived) }) } func TestTopicStreamReaderImpl_Create(t *testing.T) { xtest.TestManyTimesWithName(t, "BadSessionInitialization", func(t testing.TB) { mc := gomock.NewController(t) stream := NewMockRawTopicReaderStream(mc) stream.EXPECT().Send(gomock.Any()).Return(nil) stream.EXPECT().Recv().Return(&rawtopicreader.StartPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusInternalError}, }, nil) stream.EXPECT().CloseSend().Return(nil) reader, err := newTopicStreamReader(nil, topicreadercommon.NextReaderID(), stream, newTopicStreamReaderConfig()) require.Error(t, err) require.Nil(t, reader) }) } func TestTopicStreamReaderImpl_WaitInit(t *testing.T) { t.Run("OK", func(t *testing.T) { e := newTopicReaderTestEnv(t) e.Start() err := e.reader.WaitInit(context.Background()) require.NoError(t, err) }) t.Run("not started", func(t *testing.T) { e := newTopicReaderTestEnv(t) err := e.reader.WaitInit(context.Background()) require.Error(t, err) }) } func TestStreamReaderImpl_OnPartitionCloseHandle(t *testing.T) { xtest.TestManyTimesWithName(t, "GracefulFalseCancelPartitionContext", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() require.NoError(t, e.partitionSession.Context().Err()) // stop partition e.SendFromServerAndSetNextCallback( &rawtopicreader.StopPartitionSessionRequest{PartitionSessionID: e.partitionSessionID}, func() { require.Error(t, e.partitionSession.Context().Err()) }) e.WaitMessageReceived() }) xtest.TestManyTimesWithName(t, "TraceGracefulTrue", func(t testing.TB) { e := newTopicReaderTestEnv(t) readMessagesCtx, readMessagesCtxCancel := xcontext.WithCancel(context.Background()) committedOffset := int64(222) e.reader.cfg.Trace.OnReaderPartitionReadStopResponse = func(info trace.TopicReaderPartitionReadStopResponseStartInfo) func(doneInfo trace.TopicReaderPartitionReadStopResponseDoneInfo) { //nolint:lll expected := trace.TopicReaderPartitionReadStopResponseStartInfo{ ReaderConnectionID: e.reader.readConnectionID, PartitionContext: e.partitionSession.Context(), Topic: e.partitionSession.Topic, PartitionID: e.partitionSession.PartitionID, PartitionSessionID: e.partitionSession.StreamPartitionSessionID.ToInt64(), CommittedOffset: committedOffset, Graceful: true, } require.Equal(t, expected, info) require.NoError(t, info.PartitionContext.Err()) readMessagesCtxCancel() return nil } e.Start() stopPartitionResponseSent := make(empty.Chan) e.stream.EXPECT().Send(&rawtopicreader.StopPartitionSessionResponse{ PartitionSessionID: e.partitionSessionID, }).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(stopPartitionResponseSent) return nil }) e.SendFromServer(&rawtopicreader.StopPartitionSessionRequest{ PartitionSessionID: e.partitionSessionID, Graceful: true, CommittedOffset: rawtopiccommon.NewOffset(committedOffset), }) _, err := e.reader.ReadMessageBatch(readMessagesCtx, newReadMessageBatchOptions()) require.Error(t, err) require.Error(t, readMessagesCtx.Err()) xtest.WaitChannelClosed(t, stopPartitionResponseSent) }) xtest.TestManyTimesWithName(t, "TraceGracefulFalse", func(t testing.TB) { e := newTopicReaderTestEnv(t) readMessagesCtx, readMessagesCtxCancel := xcontext.WithCancel(context.Background()) committedOffset := int64(222) e.reader.cfg.Trace.OnReaderPartitionReadStopResponse = func(info trace.TopicReaderPartitionReadStopResponseStartInfo) func(doneInfo trace.TopicReaderPartitionReadStopResponseDoneInfo) { //nolint:lll expected := trace.TopicReaderPartitionReadStopResponseStartInfo{ ReaderConnectionID: e.reader.readConnectionID, PartitionContext: e.partitionSession.Context(), Topic: e.partitionSession.Topic, PartitionID: e.partitionSession.PartitionID, PartitionSessionID: e.partitionSession.StreamPartitionSessionID.ToInt64(), CommittedOffset: committedOffset, Graceful: false, } require.Equal(t, expected, info) require.Error(t, info.PartitionContext.Err()) readMessagesCtxCancel() return nil } e.Start() e.SendFromServer(&rawtopicreader.StopPartitionSessionRequest{ PartitionSessionID: e.partitionSessionID, Graceful: false, CommittedOffset: rawtopiccommon.NewOffset(committedOffset), }) _, err := e.reader.ReadMessageBatch(readMessagesCtx, newReadMessageBatchOptions()) require.Error(t, err) require.Error(t, readMessagesCtx.Err()) }) } func TestStreamReaderImpl_TestEndSessionForSplitMergePartitions(t *testing.T) { t.Run("Split", func(t *testing.T) { xtest.TestManyTimes(t, func(t testing.TB) { e := newTopicReaderTestEnv(t) // doesn't check sends e.stream.EXPECT().Send(gomock.Any()).Return(nil).MinTimes(0) e.Start() activePartitionID := e.partitionSession.PartitionID e.SendFromServer(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("OK")}}, }}, }, }, }) e.SendFromServer(&rawtopicreader.EndPartitionSession{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSessionID: e.partitionSessionID, ChildPartitionIDs: []int64{activePartitionID + 1, activePartitionID + 2}, }) e.SendFromServer(&rawtopicreader.StartPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSession: rawtopicreader.PartitionSession{ PartitionSessionID: e.partitionSessionID + 1, Path: e.partitionSession.Topic, PartitionID: activePartitionID + 1, }, }) e.SendFromServer(&rawtopicreader.StartPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSession: rawtopicreader.PartitionSession{ PartitionSessionID: e.partitionSessionID + 2, Path: e.partitionSession.Topic, PartitionID: activePartitionID + 2, }, }) e.SendFromServer(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID + 1, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("BAD-1")}}, }}, }, }, }) allMessagesSent := make(empty.Chan) e.SendFromServerAndSetNextCallback(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID + 2, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("BAD-2")}}, }}, }, }, }, func() { close(allMessagesSent) }) <-allMessagesSent batch, err := e.reader.ReadMessageBatch(e.ctx, newReadMessageBatchOptions()) require.NoError(t, err) require.Same(t, e.partitionSession, topicreadercommon.BatchGetPartitionSession(batch)) }) }) t.Run("Merge", func(t *testing.T) { xtest.TestManyTimes(t, func(t testing.TB) { e := newTopicReaderTestEnv(t) // doesn't check sends e.stream.EXPECT().Send(gomock.Any()).Return(nil).MinTimes(0) e.Start() activePartitionID := e.partitionSession.PartitionID e.SendFromServer(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("OK")}}, }}, }, }, }) e.SendFromServer(&rawtopicreader.EndPartitionSession{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSessionID: e.partitionSessionID, ChildPartitionIDs: []int64{activePartitionID + 1, activePartitionID + 2}, }) e.SendFromServer(&rawtopicreader.StartPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSession: rawtopicreader.PartitionSession{ PartitionSessionID: e.partitionSessionID + 1, Path: e.partitionSession.Topic, PartitionID: activePartitionID + 1, }, }) e.SendFromServer(&rawtopicreader.StartPartitionSessionRequest{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionSession: rawtopicreader.PartitionSession{ PartitionSessionID: e.partitionSessionID + 2, Path: e.partitionSession.Topic, PartitionID: activePartitionID + 2, }, }) e.SendFromServer(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID + 1, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("BAD-1")}}, }}, }, }, }) allMessagesSent := make(empty.Chan) e.SendFromServerAndSetNextCallback(&rawtopicreader.ReadResponse{ ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess}, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID + 2, Batches: []rawtopicreader.Batch{{ Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{{Offset: 1, Data: []byte("BAD-2")}}, }}, }, }, }, func() { close(allMessagesSent) }) <-allMessagesSent batch, err := e.reader.ReadMessageBatch(e.ctx, newReadMessageBatchOptions()) require.NoError(t, err) require.Same(t, e.partitionSession, topicreadercommon.BatchGetPartitionSession(batch)) }) }) } func TestTopicStreamReaderImpl_ReadMessages(t *testing.T) { t.Run("BufferSize", func(t *testing.T) { waitChangeRestBufferSizeBytes := func(r *topicStreamReaderImpl, old int64) { xtest.SpinWaitCondition(t, nil, func() bool { return r.restBufferSizeBytes.Load() != old }) } xtest.TestManyTimesWithName(t, "InitialBufferSize", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() waitChangeRestBufferSizeBytes(e.reader, 0) require.Equal(t, e.initialBufferSizeBytes, e.reader.restBufferSizeBytes.Load()) }) xtest.TestManyTimesWithName(t, "DecrementIncrementBufferSize", func(t testing.TB) { e := newTopicReaderTestEnv(t) // doesn't check sends e.stream.EXPECT().Send(gomock.Any()).Return(nil).MinTimes(1) e.Start() waitChangeRestBufferSizeBytes(e.reader, 0) const dataSize = 1000 e.SendFromServer(&rawtopicreader.ReadResponse{BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { MessageData: []rawtopicreader.MessageData{ { Offset: 1, SeqNo: 1, Data: []byte{1, 2}, }, { Offset: 2, SeqNo: 2, Data: []byte{4, 5, 6}, }, { Offset: 3, SeqNo: 3, Data: []byte{7}, }, }, }, }, }, }}) waitChangeRestBufferSizeBytes(e.reader, e.initialBufferSizeBytes) expectedBufferSizeAfterReceiveMessages := e.initialBufferSizeBytes - dataSize require.Equal(t, expectedBufferSizeAfterReceiveMessages, e.reader.restBufferSizeBytes.Load()) oneOption := newReadMessageBatchOptions() oneOption.MaxCount = 1 _, err := e.reader.ReadMessageBatch(e.ctx, oneOption) require.NoError(t, err) waitChangeRestBufferSizeBytes(e.reader, expectedBufferSizeAfterReceiveMessages) bufferSizeAfterReadOneMessage := e.reader.restBufferSizeBytes.Load() _, err = e.reader.ReadMessageBatch(e.ctx, newReadMessageBatchOptions()) require.NoError(t, err) waitChangeRestBufferSizeBytes(e.reader, bufferSizeAfterReadOneMessage) require.Equal(t, e.initialBufferSizeBytes, e.reader.restBufferSizeBytes.Load()) }) xtest.TestManyTimesWithName(t, "ForceReturnBatchIfBufferFull", func(t testing.TB) { e := newTopicReaderTestEnv(t) dataRequested := make(empty.Chan) e.stream.EXPECT().Send( &rawtopicreader.ReadRequest{BytesSize: int(e.initialBufferSizeBytes)}, ). DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(dataRequested) return nil }) e.Start() waitChangeRestBufferSizeBytes(e.reader, 0) e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: int(e.initialBufferSizeBytes), PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { MessageData: []rawtopicreader.MessageData{ { Offset: 1, SeqNo: 1, Data: []byte{1, 2, 3}, }, }, }, }, }, }, }) needReadTwoMessages := newReadMessageBatchOptions() needReadTwoMessages.MinCount = 2 readTimeoutCtx, cancel := xcontext.WithTimeout(e.ctx, time.Second) defer cancel() batch, err := e.reader.ReadMessageBatch(readTimeoutCtx, needReadTwoMessages) require.NoError(t, err) require.Len(t, batch.Messages, 1) <-dataRequested }) }) xtest.TestManyTimesWithName(t, "ReadBatch", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() compress := func(msg string) []byte { b := &bytes.Buffer{} writer := gzip.NewWriter(b) _, err := writer.Write([]byte(msg)) require.NoError(t, writer.Close()) require.NoError(t, err) return b.Bytes() } prevOffset := e.partitionSession.LastReceivedMessageOffset() sendDataRequestCompleted := make(empty.Chan) dataSize := 6 e.stream.EXPECT().Send( &rawtopicreader.ReadRequest{BytesSize: dataSize}, ).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(sendDataRequestCompleted) return nil }) e.SendFromServer(&rawtopicreader.ReadResponse{ BytesSize: dataSize, PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, WriteSessionMeta: map[string]string{"a": "b", "c": "d"}, WrittenAt: testTime(5), MessageData: []rawtopicreader.MessageData{ { Offset: prevOffset + 1, SeqNo: 1, CreatedAt: testTime(1), Data: []byte("123"), UncompressedSize: 3, MessageGroupID: "1", }, { Offset: prevOffset + 2, SeqNo: 2, CreatedAt: testTime(2), Data: []byte("4567"), UncompressedSize: 4, MessageGroupID: "1", }, }, }, { Codec: rawtopiccommon.CodecGzip, WriteSessionMeta: map[string]string{"e": "f", "g": "h"}, WrittenAt: testTime(6), MessageData: []rawtopicreader.MessageData{ { Offset: prevOffset + 10, SeqNo: 3, CreatedAt: testTime(3), Data: compress("098"), UncompressedSize: 3, MessageGroupID: "2", }, { Offset: prevOffset + 20, SeqNo: 4, CreatedAt: testTime(4), Data: compress("0987"), UncompressedSize: 4, MessageGroupID: "2", }, }, }, { Codec: rawtopiccommon.CodecRaw, WriteSessionMeta: map[string]string{"a": "b", "c": "d"}, WrittenAt: testTime(7), MessageData: []rawtopicreader.MessageData{ { Offset: prevOffset + 30, SeqNo: 5, CreatedAt: testTime(5), Data: []byte("test"), UncompressedSize: 4, MessageGroupID: "1", MetadataItems: []rawtopiccommon.MetadataItem{ { Key: "first", Value: []byte("first-value"), }, { Key: "second", Value: []byte("second-value"), }, }, }, { Offset: prevOffset + 31, SeqNo: 6, CreatedAt: testTime(5), Data: []byte("4567"), UncompressedSize: 4, MessageGroupID: "1", MetadataItems: []rawtopiccommon.MetadataItem{ { Key: "doubled-key", Value: []byte("bad"), }, { Key: "doubled-key", Value: []byte("good"), }, }, }, }, }, }, }, }, }, ) expectedData := [][]byte{[]byte("123"), []byte("4567"), []byte("098"), []byte("0987"), []byte("test"), []byte("4567")} expectedBatch := topicreadercommon.BatchSetCommitRangeForTest(&topicreadercommon.PublicBatch{ Messages: []*topicreadercommon.PublicMessage{ topicreadercommon.NewPublicMessageBuilder(). Seqno(1). CreatedAt(testTime(1)). MessageGroupID("1"). Offset(prevOffset.ToInt64() + 1). WrittenAt(testTime(5)). WriteSessionMetadata(map[string]string{"a": "b", "c": "d"}). UncompressedSize(3). RawDataLen(3). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 1, CommitOffsetEnd: prevOffset + 2, PartitionSession: e.partitionSession, }).Build(), topicreadercommon.NewPublicMessageBuilder(). Seqno(2). CreatedAt(testTime(2)). MessageGroupID("1"). Offset(prevOffset.ToInt64() + 2). WrittenAt(testTime(5)). WriteSessionMetadata(map[string]string{"a": "b", "c": "d"}). UncompressedSize(4). RawDataLen(4). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 2, CommitOffsetEnd: prevOffset + 3, PartitionSession: e.partitionSession, }).Build(), topicreadercommon.NewPublicMessageBuilder(). Seqno(3). CreatedAt(testTime(3)). MessageGroupID("2"). Offset(prevOffset.ToInt64() + 10). WrittenAt(testTime(6)). WriteSessionMetadata(map[string]string{"e": "f", "g": "h"}). UncompressedSize(3). RawDataLen(len(compress("098"))). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 3, CommitOffsetEnd: prevOffset + 11, PartitionSession: e.partitionSession, }).Build(), topicreadercommon.NewPublicMessageBuilder(). Seqno(4). CreatedAt(testTime(4)). MessageGroupID("2"). Offset(prevOffset.ToInt64() + 20). WrittenAt(testTime(6)). WriteSessionMetadata(map[string]string{"e": "f", "g": "h"}). UncompressedSize(4). RawDataLen(len(compress("0987"))). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 11, CommitOffsetEnd: prevOffset + 21, PartitionSession: e.partitionSession, }).Build(), topicreadercommon.NewPublicMessageBuilder(). Seqno(5). CreatedAt(testTime(5)). MessageGroupID("1"). Metadata(map[string][]byte{ "first": []byte("first-value"), "second": []byte("second-value"), }). Offset(prevOffset.ToInt64() + 30). WrittenAt(testTime(7)). WriteSessionMetadata(map[string]string{"a": "b", "c": "d"}). UncompressedSize(4). RawDataLen(4). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 21, CommitOffsetEnd: prevOffset + 31, PartitionSession: e.partitionSession, }).Build(), topicreadercommon.NewPublicMessageBuilder(). Seqno(6). CreatedAt(testTime(5)). MessageGroupID("1"). Metadata(map[string][]byte{ "doubled-key": []byte("good"), }). Offset(prevOffset.ToInt64() + 31). WrittenAt(testTime(7)). WriteSessionMetadata(map[string]string{"a": "b", "c": "d"}). UncompressedSize(4). RawDataLen(4). CommitRange(topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 31, CommitOffsetEnd: prevOffset + 32, PartitionSession: e.partitionSession, }).Build(), }, }, topicreadercommon.CommitRange{ CommitOffsetStart: prevOffset + 1, CommitOffsetEnd: prevOffset + 32, PartitionSession: e.partitionSession, }) opts := newReadMessageBatchOptions() opts.MinCount = 6 batch, err := e.reader.ReadMessageBatch(e.ctx, opts) require.NoError(t, err) data := make([][]byte, 0, len(batch.Messages)) for i := range batch.Messages { content, err := io.ReadAll(batch.Messages[i]) require.NoError(t, err) data = append(data, content) topicreadercommon.MessageSetNilDataForTest(batch.Messages[i]) } require.Equal(t, expectedData, data) require.Equal(t, expectedBatch, batch) <-sendDataRequestCompleted }) } func TestTopicStreamReadImpl_BatchReaderWantMoreMessagesThenBufferCanHold(t *testing.T) { sendMessageWithFullBuffer := func(e *streamEnv) empty.Chan { nextDataRequested := make(empty.Chan) e.stream.EXPECT().Send( &rawtopicreader.ReadRequest{BytesSize: int(e.initialBufferSizeBytes)}, ).DoAndReturn(func(_ rawtopicreader.ClientMessage) error { close(nextDataRequested) return nil }) e.SendFromServer( &rawtopicreader.ReadResponse{ BytesSize: int(e.initialBufferSizeBytes), PartitionData: []rawtopicreader.PartitionData{ { PartitionSessionID: e.partitionSessionID, Batches: []rawtopicreader.Batch{ { Codec: rawtopiccommon.CodecRaw, MessageData: []rawtopicreader.MessageData{ { Offset: 1, }, }, }, }, }, }, }) return nextDataRequested } xtest.TestManyTimesWithName(t, "ReadAfterMessageInBuffer", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() nextDataRequested := sendMessageWithFullBuffer(&e) // wait message received to internal buffer xtest.SpinWaitCondition(t, &e.reader.batcher.m, func() bool { return len(e.reader.batcher.messages) > 0 }) xtest.SpinWaitCondition(t, nil, func() bool { return e.reader.restBufferSizeBytes.Load() == 0 }) opts := newReadMessageBatchOptions() opts.MinCount = 2 readCtx, cancel := xcontext.WithTimeout(e.ctx, time.Second) defer cancel() batch, err := e.reader.ReadMessageBatch(readCtx, opts) require.NoError(t, err) require.Len(t, batch.Messages, 1) require.Equal(t, int64(1), batch.Messages[0].Offset) <-nextDataRequested require.Equal(t, e.initialBufferSizeBytes, e.reader.restBufferSizeBytes.Load()) }) xtest.TestManyTimesWithName(t, "ReadBeforeMessageInBuffer", func(t testing.TB) { e := newTopicReaderTestEnv(t) e.Start() readCompleted := make(empty.Chan) var batch *topicreadercommon.PublicBatch var readErr error go func() { defer close(readCompleted) opts := newReadMessageBatchOptions() opts.MinCount = 2 readCtx, cancel := xcontext.WithTimeout(e.ctx, time.Second) defer cancel() batch, readErr = e.reader.ReadMessageBatch(readCtx, opts) }() // wait to start pop e.reader.batcher.notifyAboutNewMessages() xtest.SpinWaitCondition(t, &e.reader.batcher.m, func() bool { return len(e.reader.batcher.hasNewMessages) == 0 }) nextDataRequested := sendMessageWithFullBuffer(&e) <-readCompleted require.NoError(t, readErr) require.Len(t, batch.Messages, 1) require.Equal(t, int64(1), batch.Messages[0].Offset) <-nextDataRequested require.Equal(t, e.initialBufferSizeBytes, e.reader.restBufferSizeBytes.Load()) }) } func TestTopicStreamReadImpl_CommitWithBadSession(t *testing.T) { commitByMode := func(mode topicreadercommon.PublicCommitMode) error { sleep := func() { time.Sleep(time.Second / 10) } e := newTopicReaderTestEnv(t) e.reader.cfg.CommitMode = mode e.Start() cr := topicreadercommon.CommitRange{ PartitionSession: topicreadercommon.NewPartitionSession( context.Background(), "asd", 123, topicreadercommon.NextReaderID(), "bad-connection-id", 222, 322, 213, ), } commitErr := e.reader.Commit(e.ctx, cr) sleep() require.False(t, e.reader.closed) return commitErr } t.Run("CommitModeNone", func(t *testing.T) { require.ErrorIs( t, commitByMode(topicreadercommon.CommitModeNone), topicreadercommon.ErrCommitDisabled, ) }) t.Run("CommitModeSync", func(t *testing.T) { require.ErrorIs( t, commitByMode(topicreadercommon.CommitModeSync), topicreadercommon.ErrPublicCommitSessionToExpiredSession, ) }) t.Run("CommitModeAsync", func(t *testing.T) { require.NoError(t, commitByMode(topicreadercommon.CommitModeAsync)) }) } type streamEnv struct { TopicClient *MockTopicClient ctx context.Context //nolint:containedctx t testing.TB reader *topicStreamReaderImpl stopReadEvents empty.Chan stopReadEventsCloseOnce sync.Once stream *MockRawTopicReaderStream partitionSessionID partitionSessionID mc *gomock.Controller partitionSession *topicreadercommon.PartitionSession initialBufferSizeBytes int64 m xsync.Mutex messagesFromServerToClient chan testStreamResult nextMessageNeedCallback func() } type testStreamResult struct { nextMessageCallback func() msg rawtopicreader.ServerMessage err error waitOnly bool } func newTopicReaderTestEnv(t testing.TB) streamEnv { ctx := xtest.Context(t) mc := gomock.NewController(t) stream := NewMockRawTopicReaderStream(mc) const initialBufferSizeBytes = 1000000 cfg := newTopicStreamReaderConfig() cfg.BaseContext = ctx cfg.BufferSizeProtoBytes = initialBufferSizeBytes cfg.CommitterBatchTimeLag = 0 topicClientMock := NewMockTopicClient(mc) reader := newTopicStreamReaderStopped(topicClientMock, topicreadercommon.NextReaderID(), stream, cfg) // reader.initSession() - skip stream level initialization const testPartitionID = 5 const testSessionID = 15 const testClientSessionID = 115 const testSessionComitted = 20 session := topicreadercommon.NewPartitionSession( ctx, "/test", testPartitionID, reader.readerID, reader.readConnectionID, testSessionID, testClientSessionID, testSessionComitted, ) require.NoError(t, reader.sessionController.Add(session)) env := streamEnv{ TopicClient: topicClientMock, ctx: ctx, t: t, initialBufferSizeBytes: initialBufferSizeBytes, reader: reader, stopReadEvents: make(empty.Chan), stream: stream, messagesFromServerToClient: make(chan testStreamResult), partitionSession: session, partitionSessionID: session.StreamPartitionSessionID, mc: mc, } stream.EXPECT().Recv().AnyTimes().DoAndReturn(env.receiveMessageHandler) // initial data request stream.EXPECT().Send(&rawtopicreader.ReadRequest{BytesSize: initialBufferSizeBytes}).MaxTimes(1) // allow in test send data without explicit sizes stream.EXPECT().Send(&rawtopicreader.ReadRequest{BytesSize: 0}).AnyTimes() streamClosed := make(empty.Chan) stream.EXPECT().CloseSend().Return(nil).DoAndReturn(func() error { close(streamClosed) env.closeStream() return nil }) t.Cleanup(func() { env.closeStream() _ = env.reader.CloseWithError(ctx, errors.New("test finished")) xtest.WaitChannelClosed(t, streamClosed) }) t.Cleanup(func() { if messLen := len(env.messagesFromServerToClient); messLen != 0 { t.Fatalf("not all messages consumed from server: %v", messLen) } }) //nolint:govet return env } func (e *streamEnv) Start() { require.NoError(e.t, e.reader.startBackgroundWorkers()) xtest.SpinWaitCondition(e.t, nil, func() bool { return e.reader.restBufferSizeBytes.Load() == e.initialBufferSizeBytes }) } func (e *streamEnv) SendFromServer(msg rawtopicreader.ServerMessage) { e.SendFromServerAndSetNextCallback(msg, nil) } func (e *streamEnv) SendFromServerAndSetNextCallback(msg rawtopicreader.ServerMessage, callback func()) { if msg.StatusData().Status == 0 { msg.SetStatus(rawydb.StatusSuccess) } e.messagesFromServerToClient <- testStreamResult{msg: msg, nextMessageCallback: callback} } func (e *streamEnv) WaitMessageReceived() { e.messagesFromServerToClient <- testStreamResult{waitOnly: true} } func (e *streamEnv) receiveMessageHandler() (rawtopicreader.ServerMessage, error) { if e.ctx.Err() != nil { return nil, e.ctx.Err() } var callback func() e.m.WithLock(func() { callback = e.nextMessageNeedCallback e.nextMessageNeedCallback = nil }) if callback != nil { callback() } readMessages: for { select { case <-e.ctx.Done(): return nil, e.ctx.Err() case <-e.stopReadEvents: return nil, xerrors.Wrap(errors.New("mock reader closed")) case res := <-e.messagesFromServerToClient: if res.waitOnly { continue readMessages } e.m.WithLock(func() { e.nextMessageNeedCallback = res.nextMessageCallback }) return res.msg, res.err } } } func (e *streamEnv) closeStream() { e.stopReadEventsCloseOnce.Do(func() { close(e.stopReadEvents) }) } func TestUpdateCommitInTransaction(t *testing.T) { t.Run("OK", func(t *testing.T) { e := newTopicReaderTestEnv(t) e.Start() initialCommitOffset := e.partitionSession.CommittedOffset() txID := "test-tx-id" sessionID := "test-session-id" e.TopicClient.EXPECT().UpdateOffsetsInTransaction(gomock.Any(), &rawtopic.UpdateOffsetsInTransactionRequest{ OperationParams: rawydb.OperationParams{ OperationMode: rawydb.OperationParamsModeSync, }, Tx: rawtopiccommon.TransactionIdentity{ ID: txID, Session: sessionID, }, Topics: []rawtopic.UpdateOffsetsInTransactionRequest_TopicOffsets{ { Path: e.partitionSession.Topic, Partitions: []rawtopic.UpdateOffsetsInTransactionRequest_PartitionOffsets{ { PartitionID: e.partitionSession.PartitionID, PartitionOffsets: []rawtopiccommon.OffsetRange{ { Start: initialCommitOffset, End: initialCommitOffset + 1, }, }, }, }, }, }, Consumer: e.reader.cfg.Consumer, }) txMock := newMockTransactionWrapper(sessionID, txID) batch, err := topicreadercommon.NewBatch(e.partitionSession, []*topicreadercommon.PublicMessage{ topicreadercommon.NewPublicMessageBuilder(). Offset(e.partitionSession.CommittedOffset().ToInt64()). PartitionSession(e.partitionSession). Build(), }) require.NoError(t, err) err = e.reader.commitWithTransaction(e.ctx, txMock, batch) require.NoError(t, err) err = txMock.onBeforeCommit[0](e.ctx) require.NoError(t, err) require.Len(t, txMock.onCompleted, 1) txMock.onCompleted[0](nil) require.True(t, txMock.materialized) require.Equal(t, initialCommitOffset+1, e.partitionSession.CommittedOffset()) }) t.Run("FailedAddCommitToTransactions", func(t *testing.T) { e := newTopicReaderTestEnv(t) e.Start() txID := "test-tx-id" sessionID := "test-session-id" testError := errors.New("test error") e.TopicClient.EXPECT().UpdateOffsetsInTransaction(gomock.Any(), gomock.Any()).Return(testError) txMock := newMockTransactionWrapper(sessionID, txID) batch, err := topicreadercommon.NewBatch(e.partitionSession, []*topicreadercommon.PublicMessage{ topicreadercommon.NewPublicMessageBuilder(). Offset(e.partitionSession.CommittedOffset().ToInt64()). PartitionSession(e.partitionSession). Build(), }) require.NoError(t, err) err = e.reader.commitWithTransaction(e.ctx, txMock, batch) require.NoError(t, err) require.NotEmpty(t, txMock.onBeforeCommit) err = txMock.onBeforeCommit[0](e.ctx) require.ErrorIs(t, err, testError) txMock.onCompleted[0](err) require.True(t, e.reader.closed) require.ErrorIs(t, e.reader.err, testError) require.Error(t, xerrors.RetryableError(e.reader.err)) require.False(t, txMock.RolledBack, "Rollback is an responsibility of tx itself") require.True(t, txMock.materialized) }) t.Run("UpdateOffsetsInTransaction must be executed on the tx Node", func(t *testing.T) { e := newTopicReaderTestEnv(t) e.Start() txMock := newMockTransactionWrapper("test-session-id", "test-tx-id") txMock.nodeID = 123 e.TopicClient.EXPECT().UpdateOffsetsInTransaction(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, _ *rawtopic.UpdateOffsetsInTransactionRequest) error { nodeID, ok := endpoint.ContextNodeID(ctx) require.True(t, ok) require.Equal(t, uint32(123), nodeID) return nil }) batch, err := topicreadercommon.NewBatch(e.partitionSession, nil) require.NoError(t, err) err = e.reader.commitWithTransaction(e.ctx, txMock, batch) require.NoError(t, err) err = txMock.onBeforeCommit[0](e.ctx) require.NoError(t, err) }) }