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