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  }