github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/internal/topic/topicwriterinternal/writer_reconnector_test.go (about)

     1  package topicwriterinternal
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"sort"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/stretchr/testify/require"
    17  	"go.uber.org/mock/gomock"
    18  
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    20  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon"
    21  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicwriter"
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawydb"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    24  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    26  )
    27  
    28  var testCommonEncoders = NewEncoderMap()
    29  
    30  func TestWriterImpl_AutoSeq(t *testing.T) {
    31  	t.Run("OK", func(t *testing.T) {
    32  		ctx := xtest.Context(t)
    33  		w := newWriterReconnectorStopped(newWriterReconnectorConfig(
    34  			WithAutoSetSeqNo(true),
    35  			WithAutosetCreatedTime(false),
    36  		))
    37  		w.firstConnectionHandled.Store(true)
    38  
    39  		lastSeqNo := int64(16)
    40  		w.lastSeqNo = lastSeqNo
    41  
    42  		var wg sync.WaitGroup
    43  		fWrite := func(num int) {
    44  			defer wg.Done()
    45  
    46  			msgs := newTestMessages(0)
    47  			msgs[0].CreatedAt = time.Unix(int64(num), 0)
    48  			require.NoError(t, w.Write(ctx, msgs))
    49  		}
    50  
    51  		const messCount = 1000
    52  		wg.Add(messCount)
    53  		for i := 0; i < messCount; i++ {
    54  			go fWrite(i)
    55  		}
    56  		wg.Wait()
    57  
    58  		require.Len(t, w.queue.messagesByOrder, messCount)
    59  		require.Equal(t, lastSeqNo+messCount, w.queue.lastSeqNo)
    60  	})
    61  
    62  	t.Run("PredefinedSeqNo", func(t *testing.T) {
    63  		ctx := xtest.Context(t)
    64  
    65  		w := newWriterReconnectorStopped(newWriterReconnectorConfig(WithAutoSetSeqNo(true)))
    66  		w.firstConnectionHandled.Store(true)
    67  		require.Error(t, w.Write(ctx, newTestMessages(1)))
    68  	})
    69  }
    70  
    71  func TestWriterImpl_CheckMessages(t *testing.T) {
    72  	t.Run("MessageSize", func(t *testing.T) {
    73  		ctx := xtest.Context(t)
    74  		w := newWriterReconnectorStopped(newWriterReconnectorConfig())
    75  		w.firstConnectionHandled.Store(true)
    76  
    77  		maxSize := 5
    78  		w.cfg.MaxMessageSize = maxSize
    79  
    80  		err := w.Write(ctx, []PublicMessage{{Data: bytes.NewReader(make([]byte, maxSize))}})
    81  		require.NoError(t, err)
    82  
    83  		err = w.Write(ctx, []PublicMessage{{Data: bytes.NewReader(make([]byte, maxSize+1))}})
    84  		require.Error(t, err)
    85  	})
    86  }
    87  
    88  func TestWriterImpl_Write(t *testing.T) {
    89  	t.Run("PushToQueue", func(t *testing.T) {
    90  		ctx := context.Background()
    91  		w := newTestWriterStopped()
    92  		w.cfg.AutoSetCreatedTime = false
    93  		w.firstConnectionHandled.Store(true)
    94  
    95  		err := w.Write(ctx, newTestMessages(1, 3, 5))
    96  		require.NoError(t, err)
    97  
    98  		expectedMap := map[int]messageWithDataContent{
    99  			1: newTestMessageWithDataContent(1),
   100  			2: newTestMessageWithDataContent(3),
   101  			3: newTestMessageWithDataContent(5),
   102  		}
   103  
   104  		for k := range expectedMap {
   105  			mess := expectedMap[k]
   106  			_, err = mess.GetEncodedBytes(rawtopiccommon.CodecRaw)
   107  			require.NoError(t, err)
   108  			mess.metadataCached = true
   109  			expectedMap[k] = mess
   110  		}
   111  
   112  		require.Equal(t, expectedMap, w.queue.messagesByOrder)
   113  	})
   114  	t.Run("WriteWithSyncMode", func(t *testing.T) {
   115  		xtest.TestManyTimes(t, func(t testing.TB) {
   116  			e := newTestEnv(t, &testEnvOptions{
   117  				writerOptions: []PublicWriterOption{
   118  					WithWaitAckOnWrite(true),
   119  				},
   120  			})
   121  
   122  			messageTime := time.Date(2022, 9, 7, 11, 34, 0, 0, time.UTC)
   123  			messageData := []byte("123")
   124  
   125  			const seqNo = 31
   126  
   127  			writeMessageReceived := make(empty.Chan)
   128  			e.stream.EXPECT().Send(&rawtopicwriter.WriteRequest{
   129  				Messages: []rawtopicwriter.MessageData{
   130  					{
   131  						SeqNo:            seqNo,
   132  						CreatedAt:        messageTime,
   133  						UncompressedSize: int64(len(messageData)),
   134  						Partitioning:     rawtopicwriter.Partitioning{},
   135  						Data:             messageData,
   136  					},
   137  				},
   138  				Codec: rawtopiccommon.CodecRaw,
   139  			}).Do(func(_ interface{}) {
   140  				close(writeMessageReceived)
   141  			}).Return(nil)
   142  
   143  			writeCompleted := make(empty.Chan)
   144  			go func() {
   145  				err := e.writer.Write(e.ctx, []PublicMessage{{
   146  					SeqNo:     seqNo,
   147  					CreatedAt: messageTime,
   148  					Data:      bytes.NewReader(messageData),
   149  				}})
   150  				require.NoError(t, err)
   151  				close(writeCompleted)
   152  			}()
   153  
   154  			<-writeMessageReceived
   155  
   156  			select {
   157  			case <-writeCompleted:
   158  				t.Fatal("sync write must complete after receive ack only")
   159  			default:
   160  				// pass
   161  			}
   162  
   163  			e.sendFromServer(&rawtopicwriter.WriteResult{
   164  				Acks: []rawtopicwriter.WriteAck{
   165  					{
   166  						SeqNo: seqNo,
   167  						MessageWriteStatus: rawtopicwriter.MessageWriteStatus{
   168  							Type:          rawtopicwriter.WriteStatusTypeWritten,
   169  							WrittenOffset: 4,
   170  						},
   171  					},
   172  				},
   173  				PartitionID: e.partitionID,
   174  			})
   175  
   176  			xtest.WaitChannelClosed(t, writeCompleted)
   177  		})
   178  	})
   179  }
   180  
   181  func TestWriterImpl_WriteCodecs(t *testing.T) {
   182  	t.Run("ForceRaw", func(t *testing.T) {
   183  		var err error
   184  		e := newTestEnv(t, &testEnvOptions{writerOptions: []PublicWriterOption{WithCodec(rawtopiccommon.CodecRaw)}})
   185  
   186  		messContent := []byte("123")
   187  
   188  		messReceived := make(chan rawtopiccommon.Codec, 2)
   189  		e.stream.EXPECT().Send(gomock.Any()).Do(func(message rawtopicwriter.ClientMessage) {
   190  			writeReq := message.(*rawtopicwriter.WriteRequest)
   191  			messReceived <- writeReq.Codec
   192  		})
   193  
   194  		require.NoError(t, err)
   195  		require.NoError(t, e.writer.Write(e.ctx, []PublicMessage{{
   196  			Data: bytes.NewReader(messContent),
   197  		}}))
   198  
   199  		require.Equal(t, rawtopiccommon.CodecRaw, <-messReceived)
   200  	})
   201  	t.Run("ForceGzip", func(t *testing.T) {
   202  		var err error
   203  		e := newTestEnv(t, &testEnvOptions{
   204  			writerOptions: []PublicWriterOption{WithCodec(rawtopiccommon.CodecGzip)},
   205  			topicCodecs:   rawtopiccommon.SupportedCodecs{rawtopiccommon.CodecGzip},
   206  		})
   207  
   208  		messContent := []byte("123")
   209  
   210  		gzipped := &bytes.Buffer{}
   211  		writer := gzip.NewWriter(gzipped)
   212  		_, err = writer.Write(messContent)
   213  		require.NoError(t, err)
   214  		require.NoError(t, writer.Close())
   215  
   216  		messReceived := make(chan rawtopiccommon.Codec, 2)
   217  		e.stream.EXPECT().Send(gomock.Any()).Do(func(message rawtopicwriter.ClientMessage) {
   218  			writeReq := message.(*rawtopicwriter.WriteRequest)
   219  			messReceived <- writeReq.Codec
   220  		})
   221  
   222  		require.NoError(t, err)
   223  		require.NoError(t, e.writer.Write(e.ctx, []PublicMessage{{
   224  			Data: bytes.NewReader(messContent),
   225  		}}))
   226  
   227  		require.Equal(t, rawtopiccommon.CodecGzip, <-messReceived)
   228  	})
   229  	t.Run("Auto", func(t *testing.T) {
   230  		e := newTestEnv(t, &testEnvOptions{
   231  			writerOptions: []PublicWriterOption{
   232  				WithAutoSetSeqNo(true),
   233  				WithAutoCodec(),
   234  			},
   235  			topicCodecs: rawtopiccommon.SupportedCodecs{rawtopiccommon.CodecRaw, rawtopiccommon.CodecGzip},
   236  		})
   237  
   238  		messContentShort := []byte("1")
   239  		messContentLong := make([]byte, 100000)
   240  
   241  		messReceived := make(chan rawtopiccommon.Codec, 2)
   242  		e.stream.EXPECT().Send(gomock.Any()).Do(func(message rawtopicwriter.ClientMessage) {
   243  			writeReq := message.(*rawtopicwriter.WriteRequest)
   244  			messReceived <- writeReq.Codec
   245  		}).Times(codecMeasureIntervalBatches * 2)
   246  
   247  		codecs := make(map[rawtopiccommon.Codec]empty.Struct)
   248  
   249  		for i := 0; i < codecMeasureIntervalBatches; i++ {
   250  			require.NoError(t, e.writer.Write(e.ctx, []PublicMessage{{
   251  				Data: bytes.NewReader(messContentShort),
   252  			}}))
   253  			// wait send
   254  			codec := <-messReceived
   255  			codecs[codec] = empty.Struct{}
   256  		}
   257  
   258  		for i := 0; i < codecMeasureIntervalBatches; i++ {
   259  			require.NoError(t, e.writer.Write(e.ctx, []PublicMessage{{
   260  				Data: bytes.NewReader(messContentLong),
   261  			}}))
   262  			// wait send
   263  			codec := <-messReceived
   264  			codecs[codec] = empty.Struct{}
   265  		}
   266  
   267  		// used two different codecs
   268  		require.Len(t, codecs, 2)
   269  	})
   270  }
   271  
   272  func TestWriterReconnector_Write_QueueLimit(t *testing.T) {
   273  	xtest.TestManyTimes(t, func(t testing.TB) {
   274  		ctx := xtest.Context(t)
   275  		w := newWriterReconnectorStopped(newWriterReconnectorConfig(
   276  			WithAutoSetSeqNo(false),
   277  			WithMaxQueueLen(2),
   278  		))
   279  		w.firstConnectionHandled.Store(true)
   280  
   281  		waitStartQueueWait := func(targetWaiters int) {
   282  			xtest.SpinWaitCondition(t, nil, func() bool {
   283  				res := getWaitersCount(w.semaphore) == targetWaiters
   284  
   285  				return res
   286  			})
   287  		}
   288  
   289  		err := w.Write(ctx, newTestMessages(1, 2))
   290  		require.NoError(t, err)
   291  
   292  		ctxNoQueueSpace, ctxNoQueueSpaceCancel := xcontext.WithCancel(ctx)
   293  
   294  		go func() {
   295  			waitStartQueueWait(1)
   296  			ctxNoQueueSpaceCancel()
   297  		}()
   298  		err = w.Write(ctxNoQueueSpace, newTestMessages(3))
   299  		if !errors.Is(err, PublicErrQueueIsFull) {
   300  			require.ErrorIs(t, err, PublicErrQueueIsFull)
   301  		}
   302  
   303  		go func() {
   304  			waitStartQueueWait(1)
   305  			ackErr := w.queue.AcksReceived([]rawtopicwriter.WriteAck{
   306  				{
   307  					SeqNo: 1,
   308  				},
   309  			})
   310  			require.NoError(t, ackErr)
   311  		}()
   312  
   313  		err = w.Write(ctx, newTestMessages(3))
   314  		require.NoError(t, err)
   315  	})
   316  }
   317  
   318  func TestEnv(t *testing.T) {
   319  	xtest.TestManyTimes(t, func(t testing.TB) {
   320  		env := newTestEnv(t, nil)
   321  		xtest.WaitChannelClosed(t, env.writer.firstInitResponseProcessedChan)
   322  	})
   323  }
   324  
   325  func TestWriterImpl_InitSession(t *testing.T) {
   326  	w := newTestWriterStopped(WithAutoSetSeqNo(true))
   327  	lastSeqNo := int64(123)
   328  	sessionID := "test-session-id"
   329  
   330  	w.onWriterChange(&SingleStreamWriter{
   331  		ReceivedLastSeqNum:  lastSeqNo,
   332  		LastSeqNumRequested: true,
   333  		SessionID:           sessionID,
   334  	})
   335  
   336  	require.Equal(t, sessionID, w.sessionID)
   337  	require.Equal(t, lastSeqNo, w.lastSeqNo)
   338  	require.True(t, isClosed(w.firstInitResponseProcessedChan))
   339  }
   340  
   341  func TestWriterImpl_WaitInit(t *testing.T) {
   342  	t.Run("OK", func(t *testing.T) {
   343  		w := newTestWriterStopped(WithAutoSetSeqNo(true))
   344  		expectedInitData := InitialInfo{
   345  			LastSeqNum: int64(123),
   346  		}
   347  		w.onWriterChange(&SingleStreamWriter{
   348  			ReceivedLastSeqNum:  expectedInitData.LastSeqNum,
   349  			LastSeqNumRequested: true,
   350  		})
   351  
   352  		initData, err := w.WaitInit(context.Background())
   353  		require.NoError(t, err)
   354  		require.Equal(t, expectedInitData, initData)
   355  
   356  		err = w.Write(context.Background(), newTestMessages(0))
   357  		require.NoError(t, err)
   358  
   359  		// one more run is needed to check idempotency
   360  		anotherInitData, err := w.WaitInit(context.Background())
   361  		require.NoError(t, err)
   362  		require.Equal(t, initData, anotherInitData)
   363  
   364  		require.True(t, isClosed(w.firstInitResponseProcessedChan))
   365  	})
   366  
   367  	t.Run("contextDeadlineErrorInProgress", func(t *testing.T) {
   368  		w := newTestWriterStopped(WithAutoSetSeqNo(true))
   369  		ctx, cancel := context.WithCancel(context.Background())
   370  
   371  		go func() {
   372  			// wait until w.WaitInit starts
   373  			time.Sleep(time.Millisecond)
   374  			cancel()
   375  		}()
   376  
   377  		_, err := w.WaitInit(ctx)
   378  		require.ErrorIs(t, err, ctx.Err())
   379  	})
   380  
   381  	t.Run("contextDeadlineErrorBeforeStart", func(t *testing.T) {
   382  		w := newTestWriterStopped(WithAutoSetSeqNo(true))
   383  		ctx, cancel := context.WithCancel(context.Background())
   384  		cancel()
   385  		_, err := w.WaitInit(ctx)
   386  		require.ErrorIs(t, err, ctx.Err())
   387  
   388  		w.onWriterChange(&SingleStreamWriter{})
   389  		require.True(t, isClosed(w.firstInitResponseProcessedChan))
   390  	})
   391  }
   392  
   393  func TestWriterImpl_Reconnect(t *testing.T) {
   394  	t.Run("StopReconnectOnUnretryableError", func(t *testing.T) {
   395  		mc := gomock.NewController(t)
   396  		strm := NewMockRawTopicWriterStream(mc)
   397  
   398  		w := newTestWriterStopped()
   399  
   400  		ctx := xtest.Context(t)
   401  		testErr := errors.New("test")
   402  
   403  		connectCalled := false
   404  		connectCalledChan := make(empty.Chan)
   405  
   406  		w.cfg.Connect = func(streamCtxArg context.Context) (RawTopicWriterStream, error) {
   407  			close(connectCalledChan)
   408  			connectCalled = true
   409  			require.NotEqual(t, ctx, streamCtxArg)
   410  
   411  			return strm, nil
   412  		}
   413  
   414  		initRequest := testCreateInitRequest(w)
   415  		strm.EXPECT().Send(&initRequest)
   416  		strm.EXPECT().Recv().Return(nil, testErr)
   417  		strm.EXPECT().CloseSend()
   418  
   419  		w.connectionLoop(ctx)
   420  
   421  		require.True(t, connectCalled)
   422  		require.ErrorIs(t, w.background.CloseReason(), testErr)
   423  	})
   424  
   425  	xtest.TestManyTimesWithName(t, "ReconnectOnErrors", func(t testing.TB) {
   426  		ctx := xtest.Context(t)
   427  
   428  		w := newTestWriterStopped()
   429  
   430  		mc := gomock.NewController(t)
   431  
   432  		type connectionAttemptContext struct {
   433  			name            string
   434  			stream          RawTopicWriterStream
   435  			connectionError error
   436  		}
   437  
   438  		isFirstConnection := true
   439  		newStream := func(name string) *MockRawTopicWriterStream {
   440  			strm := NewMockRawTopicWriterStream(mc)
   441  			initReq := testCreateInitRequest(w)
   442  			if isFirstConnection {
   443  				isFirstConnection = false
   444  			} else {
   445  				initReq.GetLastSeqNo = false
   446  			}
   447  
   448  			streamClosed := make(empty.Chan)
   449  			strm.EXPECT().CloseSend().Do(func() {
   450  				t.Logf("closed stream: %v", name)
   451  				close(streamClosed)
   452  			})
   453  
   454  			strm.EXPECT().Send(&initReq).Do(func(_ interface{}) {
   455  				t.Logf("sent init request stream: %v", name)
   456  			})
   457  
   458  			strm.EXPECT().Recv().Do(func() {
   459  				t.Logf("receive init response stream: %v", name)
   460  			}).Return(&rawtopicwriter.InitResult{
   461  				ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{Status: rawydb.StatusSuccess},
   462  				SessionID:             name,
   463  			}, nil)
   464  
   465  			strm.EXPECT().Recv().Do(func() {
   466  				t.Logf("waiting close channel: %v", name)
   467  				xtest.WaitChannelClosed(t, streamClosed)
   468  				t.Logf("channel closed: %v", name)
   469  			}).Return(nil, errors.New("test stream closed")).MaxTimes(1)
   470  
   471  			return strm
   472  		}
   473  
   474  		strm2 := newStream("strm2")
   475  		strm2.EXPECT().Send(&rawtopicwriter.WriteRequest{
   476  			Messages: []rawtopicwriter.MessageData{
   477  				{SeqNo: 1},
   478  			},
   479  			Codec: rawtopiccommon.CodecRaw,
   480  		}).Do(func(_ *rawtopicwriter.WriteRequest) {
   481  			t.Logf("strm2 sent message and return retriable error")
   482  		}).Return(xerrors.Retryable(errors.New("retriable on strm2")))
   483  
   484  		strm3 := newStream("strm3")
   485  		strm3.EXPECT().Send(&rawtopicwriter.WriteRequest{
   486  			Messages: []rawtopicwriter.MessageData{
   487  				{SeqNo: 1},
   488  			},
   489  			Codec: rawtopiccommon.CodecRaw,
   490  		}).Do(func(_ *rawtopicwriter.WriteRequest) {
   491  			t.Logf("strm3 sent message and return unretriable error")
   492  		}).Return(errors.New("strm3"))
   493  
   494  		connectsResult := []connectionAttemptContext{
   495  			{
   496  				name:            "step-1 connection error",
   497  				stream:          nil,
   498  				connectionError: xerrors.Retryable(errors.New("test-1")),
   499  			},
   500  			{
   501  				name:   "step-2 connect and return retryable error on write",
   502  				stream: strm2,
   503  			},
   504  			{
   505  				name:   "step-3 connect and return unretriable error on write",
   506  				stream: strm3,
   507  			},
   508  		}
   509  
   510  		var connectionAttempt atomic.Int64
   511  		w.cfg.Connect = func(ctx context.Context) (RawTopicWriterStream, error) {
   512  			attemptIndex := int(connectionAttempt.Add(1)) - 1
   513  			t.Logf("connect with attempt index: %v", attemptIndex)
   514  			res := connectsResult[attemptIndex]
   515  
   516  			return res.stream, res.connectionError
   517  		}
   518  
   519  		connectionLoopStopped := make(empty.Chan)
   520  		go func() {
   521  			defer close(connectionLoopStopped)
   522  			w.connectionLoop(ctx)
   523  			t.Log("connection loop stopped")
   524  		}()
   525  
   526  		err := w.Write(ctx, newTestMessages(1))
   527  		require.NoError(t, err)
   528  
   529  		xtest.WaitChannelClosedWithTimeout(t, connectionLoopStopped, 4*time.Second)
   530  	})
   531  }
   532  
   533  func TestAllMessagesHasSameBufCodec(t *testing.T) {
   534  	t.Run("Empty", func(t *testing.T) {
   535  		require.True(t, allMessagesHasSameBufCodec(nil))
   536  	})
   537  
   538  	t.Run("One", func(t *testing.T) {
   539  		require.True(t, allMessagesHasSameBufCodec(newTestMessagesWithContent(1)))
   540  	})
   541  
   542  	t.Run("SameCodecs", func(t *testing.T) {
   543  		require.True(t, allMessagesHasSameBufCodec(newTestMessagesWithContent(1, 2, 3)))
   544  	})
   545  	t.Run("DifferCodecs", func(t *testing.T) {
   546  		for i := 0; i < 3; i++ {
   547  			messages := newTestMessagesWithContent(1, 2, 3)
   548  			messages[i].bufCodec = rawtopiccommon.CodecGzip
   549  			require.False(t, allMessagesHasSameBufCodec(messages))
   550  		}
   551  	})
   552  }
   553  
   554  func TestCreateRawMessageData(t *testing.T) {
   555  	t.Run("Empty", func(t *testing.T) {
   556  		req, err := createWriteRequest(newTestMessagesWithContent(), rawtopiccommon.CodecRaw)
   557  		require.NoError(t, err)
   558  		require.Equal(t,
   559  			rawtopicwriter.WriteRequest{
   560  				Messages: []rawtopicwriter.MessageData{},
   561  				Codec:    rawtopiccommon.CodecRaw,
   562  			},
   563  			req,
   564  		)
   565  	})
   566  	t.Run("WithMessageMetadata", func(t *testing.T) {
   567  		messages := newTestMessagesWithContent(1)
   568  		messages[0].Metadata = map[string][]byte{
   569  			"a": {1, 2, 3},
   570  			"b": {4, 5},
   571  		}
   572  		req, err := createWriteRequest(messages, rawtopiccommon.CodecRaw)
   573  
   574  		sort.Slice(req.Messages[0].MetadataItems, func(i, j int) bool {
   575  			return req.Messages[0].MetadataItems[i].Key < req.Messages[0].MetadataItems[j].Key
   576  		})
   577  
   578  		require.NoError(t, err)
   579  		require.Equal(t, rawtopicwriter.WriteRequest{
   580  			Messages: []rawtopicwriter.MessageData{
   581  				{
   582  					SeqNo: 1,
   583  					MetadataItems: []rawtopiccommon.MetadataItem{
   584  						{
   585  							Key:   "a",
   586  							Value: []byte{1, 2, 3},
   587  						},
   588  						{
   589  							Key:   "b",
   590  							Value: []byte{4, 5},
   591  						},
   592  					},
   593  				},
   594  			},
   595  			Codec: rawtopiccommon.CodecRaw,
   596  		}, req)
   597  	})
   598  	t.Run("WithSeqno", func(t *testing.T) {
   599  		req, err := createWriteRequest(newTestMessagesWithContent(1, 2, 3), rawtopiccommon.CodecRaw)
   600  		require.NoError(t, err)
   601  		require.Equal(t,
   602  			rawtopicwriter.WriteRequest{
   603  				Messages: []rawtopicwriter.MessageData{
   604  					{
   605  						SeqNo: 1,
   606  					},
   607  					{
   608  						SeqNo: 2,
   609  					},
   610  					{
   611  						SeqNo: 3,
   612  					},
   613  				},
   614  				Codec: rawtopiccommon.CodecRaw,
   615  			},
   616  			req,
   617  		)
   618  	})
   619  }
   620  
   621  func TestSplitMessagesByBufCodec(t *testing.T) {
   622  	tests := [][]rawtopiccommon.Codec{
   623  		nil,
   624  		{},
   625  		{rawtopiccommon.CodecRaw},
   626  		{rawtopiccommon.CodecRaw, rawtopiccommon.CodecRaw},
   627  		{rawtopiccommon.CodecRaw, rawtopiccommon.CodecGzip},
   628  		{
   629  			rawtopiccommon.CodecRaw,
   630  			rawtopiccommon.CodecGzip,
   631  			rawtopiccommon.CodecGzip,
   632  			rawtopiccommon.CodecRaw,
   633  			rawtopiccommon.CodecGzip,
   634  			rawtopiccommon.CodecRaw,
   635  			rawtopiccommon.CodecRaw,
   636  		},
   637  	}
   638  
   639  	for _, test := range tests {
   640  		t.Run(fmt.Sprint(test), func(t *testing.T) {
   641  			var messages []messageWithDataContent
   642  			for index, codec := range test {
   643  				mess := newTestMessageWithDataContent(index)
   644  				mess.bufCodec = codec
   645  				messages = append(messages, mess)
   646  			}
   647  
   648  			groups := splitMessagesByBufCodec(messages)
   649  			expectedNum := int64(-1)
   650  			for _, group := range groups {
   651  				require.NotEmpty(t, group)
   652  				require.True(t, allMessagesHasSameBufCodec(group))
   653  				require.Len(t, group, cap(group))
   654  				for _, mess := range group {
   655  					expectedNum++
   656  					require.Equal(t, test[int(expectedNum)], mess.bufCodec)
   657  					mess.SeqNo = expectedNum
   658  				}
   659  			}
   660  
   661  			require.Equal(t, int(expectedNum), len(test)-1)
   662  		})
   663  	}
   664  }
   665  
   666  func TestCalculateAllowedCodecs(t *testing.T) {
   667  	customCodecSupported := rawtopiccommon.Codec(rawtopiccommon.CodecCustomerFirst)
   668  	customCodecUnsupported := rawtopiccommon.Codec(rawtopiccommon.CodecCustomerFirst + 1)
   669  	encoders := NewEncoderMap()
   670  	encoders.AddEncoder(customCodecSupported, func(writer io.Writer) (io.WriteCloser, error) {
   671  		return nil, errors.New("test")
   672  	})
   673  
   674  	table := []struct {
   675  		name           string
   676  		force          rawtopiccommon.Codec
   677  		serverCodecs   rawtopiccommon.SupportedCodecs
   678  		expectedResult rawtopiccommon.SupportedCodecs
   679  	}{
   680  		{
   681  			name:         "ForceRawWithEmptyServer",
   682  			force:        rawtopiccommon.CodecRaw,
   683  			serverCodecs: nil,
   684  			expectedResult: rawtopiccommon.SupportedCodecs{
   685  				rawtopiccommon.CodecRaw,
   686  			},
   687  		},
   688  		{
   689  			name:  "ForceRawWithAllowedByServer",
   690  			force: rawtopiccommon.CodecRaw,
   691  			serverCodecs: rawtopiccommon.SupportedCodecs{
   692  				rawtopiccommon.CodecRaw,
   693  				rawtopiccommon.CodecGzip,
   694  			},
   695  			expectedResult: rawtopiccommon.SupportedCodecs{
   696  				rawtopiccommon.CodecRaw,
   697  			},
   698  		},
   699  		{
   700  			name:  "ForceCustomWithAllowedByServer",
   701  			force: customCodecSupported,
   702  			serverCodecs: rawtopiccommon.SupportedCodecs{
   703  				rawtopiccommon.CodecRaw,
   704  				rawtopiccommon.CodecGzip,
   705  				customCodecSupported,
   706  			},
   707  			expectedResult: rawtopiccommon.SupportedCodecs{
   708  				customCodecSupported,
   709  			},
   710  		},
   711  		{
   712  			name:  "ForceRawWithDeniedByServer",
   713  			force: rawtopiccommon.CodecRaw,
   714  			serverCodecs: rawtopiccommon.SupportedCodecs{
   715  				rawtopiccommon.CodecGzip,
   716  			},
   717  			expectedResult: nil,
   718  		},
   719  		{
   720  			name:         "NotForcedWithEmptyServerList",
   721  			force:        rawtopiccommon.CodecUNSPECIFIED,
   722  			serverCodecs: nil,
   723  			expectedResult: rawtopiccommon.SupportedCodecs{
   724  				rawtopiccommon.CodecRaw,
   725  				rawtopiccommon.CodecGzip,
   726  			},
   727  		},
   728  		{
   729  			name:  "NotForcedWithServerGzipOnly",
   730  			force: rawtopiccommon.CodecUNSPECIFIED,
   731  			serverCodecs: rawtopiccommon.SupportedCodecs{
   732  				rawtopiccommon.CodecGzip,
   733  			},
   734  			expectedResult: rawtopiccommon.SupportedCodecs{
   735  				rawtopiccommon.CodecGzip,
   736  			},
   737  		},
   738  		{
   739  			name:  "NotForcedCustomCodecSupportedAndAllowedByServer",
   740  			force: rawtopiccommon.CodecUNSPECIFIED,
   741  			serverCodecs: rawtopiccommon.SupportedCodecs{
   742  				rawtopiccommon.CodecGzip,
   743  				customCodecSupported,
   744  				customCodecUnsupported,
   745  			},
   746  			expectedResult: rawtopiccommon.SupportedCodecs{
   747  				rawtopiccommon.CodecGzip,
   748  				customCodecSupported,
   749  			},
   750  		},
   751  	}
   752  
   753  	for _, test := range table {
   754  		t.Run(test.name, func(t *testing.T) {
   755  			res := calculateAllowedCodecs(test.force, encoders, test.serverCodecs)
   756  			require.Equal(t, test.expectedResult, res)
   757  		})
   758  	}
   759  }
   760  
   761  func newTestMessageWithDataContent(num int) messageWithDataContent {
   762  	res := newMessageDataWithContent(PublicMessage{SeqNo: int64(num)}, testCommonEncoders)
   763  
   764  	return res
   765  }
   766  
   767  func newTestMessages(numbers ...int) []PublicMessage {
   768  	messages := make([]PublicMessage, len(numbers))
   769  	for i, num := range numbers {
   770  		messages[i].SeqNo = int64(num)
   771  	}
   772  
   773  	return messages
   774  }
   775  
   776  func newTestMessagesWithContent(numbers ...int) []messageWithDataContent {
   777  	messages := make([]messageWithDataContent, 0, len(numbers))
   778  	for _, num := range numbers {
   779  		messages = append(messages, newTestMessageWithDataContent(num))
   780  	}
   781  
   782  	return messages
   783  }
   784  
   785  func newTestWriterStopped(opts ...PublicWriterOption) *WriterReconnector {
   786  	cfgOptions := append(defaultTestWriterOptions(), opts...)
   787  	cfg := newWriterReconnectorConfig(cfgOptions...)
   788  	res := newWriterReconnectorStopped(cfg)
   789  
   790  	if cfg.AdditionalEncoders == nil {
   791  		res.encodersMap = testCommonEncoders
   792  	}
   793  
   794  	return res
   795  }
   796  
   797  func defaultTestWriterOptions() []PublicWriterOption {
   798  	return []PublicWriterOption{
   799  		WithProducerID("test-producer-id"),
   800  		WithTopic("test-topic"),
   801  		WithSessionMeta(map[string]string{"test-key": "test-val"}),
   802  		WithPartitioning(NewPartitioningWithMessageGroupID("test-message-group-id")),
   803  		WithAutoSetSeqNo(false),
   804  		WithWaitAckOnWrite(false),
   805  		WithCodec(rawtopiccommon.CodecRaw),
   806  		WithAutosetCreatedTime(false),
   807  	}
   808  }
   809  
   810  func isClosed(ch <-chan struct{}) bool {
   811  	select {
   812  	case _, existVal := <-ch:
   813  		if existVal {
   814  			panic("value, when not expected")
   815  		}
   816  
   817  		return true
   818  	default:
   819  		return false
   820  	}
   821  }
   822  
   823  type testEnv struct {
   824  	ctx                   context.Context
   825  	stream                *MockRawTopicWriterStream
   826  	writer                *WriterReconnector
   827  	sendFromServerChannel chan sendFromServerResponse
   828  	stopReadEvents        empty.Chan
   829  	partitionID           int64
   830  	connectCount          int64
   831  }
   832  
   833  type testEnvOptions struct {
   834  	writerOptions []PublicWriterOption
   835  	lastSeqNo     int64
   836  	topicCodecs   rawtopiccommon.SupportedCodecs
   837  }
   838  
   839  func newTestEnv(t testing.TB, options *testEnvOptions) *testEnv {
   840  	if options == nil {
   841  		options = &testEnvOptions{}
   842  	}
   843  
   844  	res := &testEnv{
   845  		ctx:                   xtest.Context(t),
   846  		stream:                NewMockRawTopicWriterStream(gomock.NewController(t)),
   847  		sendFromServerChannel: make(chan sendFromServerResponse, 1),
   848  		stopReadEvents:        make(empty.Chan),
   849  		partitionID:           14,
   850  	}
   851  
   852  	writerOptions := append(defaultTestWriterOptions(), WithConnectFunc(func(ctx context.Context) (
   853  		RawTopicWriterStream,
   854  		error,
   855  	) {
   856  		connectNum := atomic.AddInt64(&res.connectCount, 1)
   857  		if connectNum > 1 {
   858  			t.Fatalf("test: default env support most one connection")
   859  		}
   860  
   861  		return res.stream, nil
   862  	}))
   863  	writerOptions = append(writerOptions, options.writerOptions...)
   864  
   865  	res.writer = newWriterReconnectorStopped(newWriterReconnectorConfig(writerOptions...))
   866  
   867  	res.stream.EXPECT().Recv().DoAndReturn(res.receiveMessageHandler).AnyTimes()
   868  
   869  	req := testCreateInitRequest(res.writer)
   870  
   871  	res.stream.EXPECT().Send(&req).Do(func(_ interface{}) {
   872  		supportedCodecs := rawtopiccommon.SupportedCodecs{rawtopiccommon.CodecRaw}
   873  		if options.topicCodecs != nil {
   874  			supportedCodecs = options.topicCodecs
   875  		}
   876  		res.sendFromServer(&rawtopicwriter.InitResult{
   877  			ServerMessageMetadata: rawtopiccommon.ServerMessageMetadata{},
   878  			LastSeqNo:             options.lastSeqNo,
   879  			SessionID:             "session-" + t.Name(),
   880  			PartitionID:           res.partitionID,
   881  			SupportedCodecs:       supportedCodecs,
   882  		})
   883  	}).Return(nil)
   884  
   885  	streamClosed := make(empty.Chan)
   886  	res.stream.EXPECT().CloseSend().Do(func() {
   887  		close(streamClosed)
   888  	})
   889  
   890  	res.writer.start()
   891  	require.NoError(t, res.writer.waitFirstInitResponse(res.ctx))
   892  
   893  	t.Cleanup(func() {
   894  		res.writer.close(context.Background(), errors.New("stop writer test environment"))
   895  		close(res.stopReadEvents)
   896  		<-streamClosed
   897  	})
   898  
   899  	return res
   900  }
   901  
   902  func (e *testEnv) sendFromServer(msg rawtopicwriter.ServerMessage) {
   903  	if msg.StatusData().Status == 0 {
   904  		msg.SetStatus(rawydb.StatusSuccess)
   905  	}
   906  
   907  	e.sendFromServerChannel <- sendFromServerResponse{msg: msg}
   908  }
   909  
   910  func (e *testEnv) receiveMessageHandler() (rawtopicwriter.ServerMessage, error) {
   911  	select {
   912  	case <-e.stopReadEvents:
   913  		return nil, fmt.Errorf("test: stop test environment")
   914  	case res := <-e.sendFromServerChannel:
   915  		return res.msg, res.err
   916  	}
   917  }
   918  
   919  type sendFromServerResponse struct {
   920  	msg rawtopicwriter.ServerMessage
   921  	err error
   922  }
   923  
   924  func testCreateInitRequest(w *WriterReconnector) rawtopicwriter.InitRequest {
   925  	req := newSingleStreamWriterStopped(context.Background(), w.createWriterStreamConfig(nil)).createInitRequest()
   926  
   927  	return req
   928  }