github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/topic/topicreaderinternal/stream_reconnector_test.go (about)

     1  package topicreaderinternal
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"go.uber.org/mock/gomock"
    11  
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/background"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/empty"
    14  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/topic"
    15  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/topic/topicreadercommon"
    16  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/value"
    17  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    18  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    20  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    21  )
    22  
    23  var _ batchedStreamReader = &readerReconnector{} // check interface implementation
    24  
    25  func TestTopicReaderReconnectorReadMessageBatch(t *testing.T) {
    26  	xtest.TestManyTimesWithName(t, "Ok", func(t testing.TB) {
    27  		mc := gomock.NewController(t)
    28  		defer mc.Finish()
    29  
    30  		baseReader := NewMockbatchedStreamReader(mc)
    31  
    32  		opts := ReadMessageBatchOptions{batcherGetOptions: batcherGetOptions{MaxCount: 10}}
    33  		batch := &topicreadercommon.PublicBatch{
    34  			Messages: []*topicreadercommon.PublicMessage{{WrittenAt: time.Date(2022, 0o6, 15, 17, 56, 0, 0, time.UTC)}},
    35  		}
    36  		baseReader.EXPECT().ReadMessageBatch(gomock.Any(), opts).Return(batch, nil)
    37  
    38  		reader := &readerReconnector{
    39  			streamVal:           baseReader,
    40  			streamContextCancel: func(cause error) {},
    41  			tracer:              &trace.Topic{},
    42  		}
    43  		reader.initChannelsAndClock()
    44  		res, err := reader.ReadMessageBatch(context.Background(), opts)
    45  		require.NoError(t, err)
    46  		require.Equal(t, batch, res)
    47  	})
    48  
    49  	xtest.TestManyTimesWithName(t, "WithConnect", func(t testing.TB) {
    50  		mc := gomock.NewController(t)
    51  
    52  		baseReader := NewMockbatchedStreamReader(mc)
    53  		opts := ReadMessageBatchOptions{batcherGetOptions: batcherGetOptions{MaxCount: 10}}
    54  		batch := &topicreadercommon.PublicBatch{
    55  			Messages: []*topicreadercommon.PublicMessage{{WrittenAt: time.Date(2022, 0o6, 15, 17, 56, 0, 0, time.UTC)}},
    56  		}
    57  		baseReader.EXPECT().ReadMessageBatch(gomock.Any(), opts).Return(batch, nil)
    58  
    59  		connectCalled := 0
    60  		reader := &readerReconnector{
    61  			readerConnect: func(ctx context.Context) (batchedStreamReader, error) {
    62  				connectCalled++
    63  				if connectCalled > 1 {
    64  					return nil, errors.New("unexpected call test connect function")
    65  				}
    66  
    67  				return baseReader, nil
    68  			},
    69  			retrySettings: topic.RetrySettings{
    70  				StartTimeout: topic.DefaultStartTimeout,
    71  			},
    72  			streamErr: errUnconnected,
    73  			tracer:    &trace.Topic{},
    74  		}
    75  		reader.initChannelsAndClock()
    76  		reader.background.Start("test-reconnectionLoop", reader.reconnectionLoop)
    77  
    78  		res, err := reader.ReadMessageBatch(context.Background(), opts)
    79  		require.NoError(t, err)
    80  		require.Equal(t, batch, res)
    81  		mc.Finish()
    82  	})
    83  
    84  	xtest.TestManyTimesWithName(t, "WithReConnect", func(t testing.TB) {
    85  		mc := gomock.NewController(t)
    86  		defer mc.Finish()
    87  
    88  		opts := ReadMessageBatchOptions{batcherGetOptions: batcherGetOptions{MaxCount: 10}}
    89  
    90  		baseReader1 := NewMockbatchedStreamReader(mc)
    91  		baseReader1.EXPECT().ReadMessageBatch(gomock.Any(), opts).MinTimes(1).
    92  			Return(nil, xerrors.Retryable(errors.New("test1")))
    93  		baseReader1.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
    94  
    95  		baseReader2 := NewMockbatchedStreamReader(mc)
    96  		baseReader2.EXPECT().ReadMessageBatch(gomock.Any(), opts).MinTimes(1).
    97  			Return(nil, xerrors.Retryable(errors.New("test2")))
    98  		baseReader2.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
    99  
   100  		baseReader3 := NewMockbatchedStreamReader(mc)
   101  		batch := &topicreadercommon.PublicBatch{
   102  			Messages: []*topicreadercommon.PublicMessage{{WrittenAt: time.Date(2022, 0o6, 15, 17, 56, 0, 0, time.UTC)}},
   103  		}
   104  		baseReader3.EXPECT().ReadMessageBatch(gomock.Any(), opts).Return(batch, nil)
   105  
   106  		readers := []batchedStreamReader{
   107  			baseReader1, baseReader2, baseReader3,
   108  		}
   109  		connectCalled := 0
   110  		reader := &readerReconnector{
   111  			readerConnect: func(ctx context.Context) (batchedStreamReader, error) {
   112  				connectCalled++
   113  
   114  				return readers[connectCalled-1], nil
   115  			},
   116  			retrySettings: topic.RetrySettings{
   117  				StartTimeout: topic.DefaultStartTimeout,
   118  			},
   119  			streamErr: errUnconnected,
   120  			tracer:    &trace.Topic{},
   121  		}
   122  		reader.initChannelsAndClock()
   123  		reader.background.Start("test-reconnectionLoop", reader.reconnectionLoop)
   124  
   125  		res, err := reader.ReadMessageBatch(context.Background(), opts)
   126  		require.NoError(t, err)
   127  		require.Equal(t, batch, res)
   128  	})
   129  
   130  	xtest.TestManyTimesWithName(t, "StartWithCancelledContext", func(t testing.TB) {
   131  		cancelledCtx, cancelledCtxCancel := xcontext.WithCancel(context.Background())
   132  		cancelledCtxCancel()
   133  
   134  		for i := 0; i < 100; i++ {
   135  			reconnector := &readerReconnector{tracer: &trace.Topic{}}
   136  			reconnector.initChannelsAndClock()
   137  
   138  			_, err := reconnector.ReadMessageBatch(cancelledCtx, ReadMessageBatchOptions{})
   139  			require.ErrorIs(t, err, context.Canceled)
   140  		}
   141  	})
   142  
   143  	xtest.TestManyTimesWithName(t, "OnClose", func(t testing.TB) {
   144  		reconnector := &readerReconnector{
   145  			tracer:    &trace.Topic{},
   146  			streamErr: errUnconnected,
   147  		}
   148  		reconnector.initChannelsAndClock()
   149  		testErr := errors.New("test'")
   150  
   151  		go func() {
   152  			_ = reconnector.CloseWithError(context.Background(), testErr)
   153  		}()
   154  
   155  		_, err := reconnector.ReadMessageBatch(context.Background(), ReadMessageBatchOptions{})
   156  		require.ErrorIs(t, err, testErr)
   157  	})
   158  }
   159  
   160  func TestTopicReaderReconnectorCommit(t *testing.T) {
   161  	type k struct{}
   162  	ctx := context.WithValue(context.Background(), k{}, "v")
   163  	expectedCommitRange := topicreadercommon.CommitRange{CommitOffsetStart: 1, CommitOffsetEnd: 2}
   164  	testErr := errors.New("test")
   165  	testErr2 := errors.New("test2")
   166  	t.Run("AllOk", func(t *testing.T) {
   167  		mc := gomock.NewController(t)
   168  		defer mc.Finish()
   169  
   170  		stream := NewMockbatchedStreamReader(mc)
   171  		stream.EXPECT().Commit(
   172  			gomock.Any(),
   173  			gomock.Any(),
   174  		).DoAndReturn(func(ctx context.Context, offset topicreadercommon.CommitRange) error {
   175  			require.Equal(t, "v", ctx.Value(k{}))
   176  			require.Equal(t, expectedCommitRange, offset)
   177  
   178  			return nil
   179  		})
   180  
   181  		reconnector := &readerReconnector{
   182  			streamVal:           stream,
   183  			streamContextCancel: func(cause error) {},
   184  			tracer:              &trace.Topic{},
   185  		}
   186  		reconnector.initChannelsAndClock()
   187  		require.NoError(t, reconnector.Commit(ctx, expectedCommitRange))
   188  	})
   189  	t.Run("StreamOkCommitErr", func(t *testing.T) {
   190  		mc := gomock.NewController(t)
   191  		stream := NewMockbatchedStreamReader(mc)
   192  		stream.EXPECT().Commit(
   193  			gomock.Any(),
   194  			gomock.Any(),
   195  		).DoAndReturn(func(ctx context.Context, offset topicreadercommon.CommitRange) error {
   196  			require.Equal(t, "v", ctx.Value(k{}))
   197  			require.Equal(t, expectedCommitRange, offset)
   198  
   199  			return testErr
   200  		})
   201  
   202  		reconnector := &readerReconnector{
   203  			streamVal:           stream,
   204  			streamContextCancel: func(cause error) {},
   205  			tracer:              &trace.Topic{},
   206  		}
   207  		reconnector.initChannelsAndClock()
   208  		require.ErrorIs(t, reconnector.Commit(ctx, expectedCommitRange), testErr)
   209  	})
   210  	t.Run("StreamErr", func(t *testing.T) {
   211  		reconnector := &readerReconnector{streamErr: testErr, tracer: &trace.Topic{}}
   212  		reconnector.initChannelsAndClock()
   213  		require.ErrorIs(t, reconnector.Commit(ctx, expectedCommitRange), testErr)
   214  	})
   215  	t.Run("CloseErr", func(t *testing.T) {
   216  		reconnector := &readerReconnector{tracer: &trace.Topic{}}
   217  		_ = reconnector.background.Close(ctx, testErr)
   218  		reconnector.initChannelsAndClock()
   219  		require.ErrorIs(t, reconnector.Commit(ctx, expectedCommitRange), testErr)
   220  	})
   221  	t.Run("StreamAndCloseErr", func(t *testing.T) {
   222  		reconnector := &readerReconnector{streamErr: testErr2, tracer: &trace.Topic{}}
   223  		_ = reconnector.background.Close(ctx, testErr)
   224  		reconnector.initChannelsAndClock()
   225  		require.ErrorIs(t, reconnector.Commit(ctx, expectedCommitRange), testErr)
   226  	})
   227  }
   228  
   229  func TestTopicReaderReconnectorConnectionLoop(t *testing.T) {
   230  	t.Run("Reconnect", func(t *testing.T) {
   231  		ctx := xtest.Context(t)
   232  		mc := gomock.NewController(t)
   233  		defer mc.Finish()
   234  
   235  		newStream1 := NewMockbatchedStreamReader(mc)
   236  		newStream1.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).MinTimes(1)
   237  		newStream2 := NewMockbatchedStreamReader(mc)
   238  
   239  		newStream2.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).MinTimes(1)
   240  
   241  		reconnector := &readerReconnector{
   242  			background: *background.NewWorker(ctx, "test-worker, "+t.Name()),
   243  			retrySettings: topic.RetrySettings{
   244  				StartTimeout: value.InfiniteDuration,
   245  			},
   246  			tracer:         &trace.Topic{},
   247  			connectTimeout: value.InfiniteDuration,
   248  		}
   249  		reconnector.initChannelsAndClock()
   250  
   251  		stream1Ready := make(empty.Chan)
   252  		stream2Ready := make(empty.Chan)
   253  		reconnector.readerConnect = readerConnectFuncMock([]readerConnectFuncAnswer{
   254  			{
   255  				callback: func(ctx context.Context) (batchedStreamReader, error) {
   256  					close(stream1Ready)
   257  
   258  					return newStream1, nil
   259  				},
   260  			},
   261  			{
   262  				err: xerrors.Retryable(errors.New("test reconnect error")),
   263  			},
   264  			{
   265  				callback: func(ctx context.Context) (batchedStreamReader, error) {
   266  					close(stream2Ready)
   267  
   268  					return newStream2, nil
   269  				},
   270  			},
   271  			{
   272  				callback: func(ctx context.Context) (batchedStreamReader, error) {
   273  					t.Fatal()
   274  
   275  					return nil, errors.New("unexpected call")
   276  				},
   277  			},
   278  		}...)
   279  
   280  		reconnector.start()
   281  
   282  		<-stream1Ready
   283  
   284  		// skip bad (old) stream
   285  		reconnector.reconnectFromBadStream <- newReconnectRequest(NewMockbatchedStreamReader(mc), nil)
   286  
   287  		reconnector.reconnectFromBadStream <- newReconnectRequest(newStream1, nil)
   288  
   289  		<-stream2Ready
   290  
   291  		// wait apply stream2 connection
   292  		xtest.SpinWaitCondition(t, &reconnector.m, func() bool {
   293  			return reconnector.streamVal == newStream2
   294  		})
   295  
   296  		require.NoError(t, reconnector.CloseWithError(ctx, errReaderClosed))
   297  	})
   298  
   299  	t.Run("StartWithCancelledContext", func(t *testing.T) {
   300  		ctx, cancel := xcontext.WithCancel(context.Background())
   301  		cancel()
   302  		reconnector := &readerReconnector{
   303  			tracer: &trace.Topic{},
   304  		}
   305  		reconnector.initChannelsAndClock()
   306  		reconnector.reconnectionLoop(ctx) // must return
   307  	})
   308  }
   309  
   310  func TestTopicReaderReconnectorStart(t *testing.T) {
   311  	mc := gomock.NewController(t)
   312  	defer mc.Finish()
   313  
   314  	ctx := context.Background()
   315  
   316  	reconnector := &readerReconnector{
   317  		tracer: &trace.Topic{},
   318  	}
   319  	reconnector.initChannelsAndClock()
   320  
   321  	stream := NewMockbatchedStreamReader(mc)
   322  	stream.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, err error) error {
   323  		require.Error(t, err)
   324  
   325  		return nil
   326  	})
   327  
   328  	connectionRequested := make(empty.Chan)
   329  	reconnector.readerConnect = readerConnectFuncMock([]readerConnectFuncAnswer{
   330  		{callback: func(ctx context.Context) (batchedStreamReader, error) {
   331  			close(connectionRequested)
   332  
   333  			return stream, nil
   334  		}},
   335  		{callback: func(ctx context.Context) (batchedStreamReader, error) {
   336  			t.Error()
   337  
   338  			return nil, errors.New("unexpected call")
   339  		}},
   340  	}...)
   341  
   342  	reconnector.start()
   343  
   344  	<-connectionRequested
   345  	_ = reconnector.CloseWithError(ctx, nil)
   346  }
   347  
   348  func TestTopicReaderReconnectorWaitInit(t *testing.T) {
   349  	t.Run("OK", func(t *testing.T) {
   350  		mc := gomock.NewController(t)
   351  		defer mc.Finish()
   352  
   353  		reconnector := &readerReconnector{
   354  			tracer: &trace.Topic{},
   355  		}
   356  		reconnector.initChannelsAndClock()
   357  
   358  		stream := NewMockbatchedStreamReader(mc)
   359  
   360  		reconnector.readerConnect = readerConnectFuncMock(readerConnectFuncAnswer{
   361  			callback: func(ctx context.Context) (batchedStreamReader, error) {
   362  				return stream, nil
   363  			},
   364  		})
   365  
   366  		reconnector.start()
   367  
   368  		err := reconnector.WaitInit(context.Background())
   369  		require.NoError(t, err)
   370  
   371  		// one more run is needed to check idempotency
   372  		err = reconnector.WaitInit(context.Background())
   373  		require.NoError(t, err)
   374  	})
   375  
   376  	t.Run("contextDeadlineInProgress", func(t *testing.T) {
   377  		xtest.TestManyTimes(t, func(t testing.TB) {
   378  			mc := gomock.NewController(t)
   379  			defer mc.Finish()
   380  
   381  			reconnector := &readerReconnector{
   382  				tracer: &trace.Topic{},
   383  			}
   384  			reconnector.initChannelsAndClock()
   385  
   386  			stream := NewMockbatchedStreamReader(mc)
   387  
   388  			waitInitReturned := make(empty.Chan)
   389  			ctx, cancel := context.WithCancel(context.Background())
   390  			reconnector.readerConnect = readerConnectFuncMock(readerConnectFuncAnswer{
   391  				callback: func(ctx context.Context) (batchedStreamReader, error) {
   392  					cancel()
   393  					<-waitInitReturned
   394  
   395  					return stream, nil
   396  				},
   397  			})
   398  			reconnector.start()
   399  
   400  			err := reconnector.WaitInit(ctx)
   401  			close(waitInitReturned)
   402  			require.ErrorIs(t, err, ctx.Err())
   403  		})
   404  	})
   405  
   406  	t.Run("contextDeadlineBeforeStart", func(t *testing.T) {
   407  		mc := gomock.NewController(t)
   408  		defer mc.Finish()
   409  
   410  		reconnector := &readerReconnector{
   411  			tracer: &trace.Topic{},
   412  		}
   413  		reconnector.initDoneCh = make(empty.Chan, 1)
   414  		reconnector.initChannelsAndClock()
   415  
   416  		stream := NewMockbatchedStreamReader(mc)
   417  
   418  		reconnector.readerConnect = readerConnectFuncMock(readerConnectFuncAnswer{
   419  			callback: func(ctx context.Context) (batchedStreamReader, error) {
   420  				return stream, nil
   421  			},
   422  		})
   423  
   424  		ctx, cancel := context.WithCancel(context.Background())
   425  		cancel()
   426  		err := reconnector.WaitInit(ctx)
   427  
   428  		require.ErrorIs(t, err, ctx.Err())
   429  	})
   430  
   431  	t.Run("UnretriableError", func(t *testing.T) {
   432  		reconnector := &readerReconnector{
   433  			tracer: &trace.Topic{},
   434  		}
   435  		reconnector.initChannelsAndClock()
   436  
   437  		testErr := errors.New("test error")
   438  		ctx := context.Background()
   439  		reconnector.readerConnect = readerConnectFuncMock(readerConnectFuncAnswer{
   440  			callback: func(ctx context.Context) (batchedStreamReader, error) {
   441  				return nil, testErr
   442  			},
   443  		})
   444  		reconnector.start()
   445  
   446  		err := reconnector.WaitInit(ctx)
   447  		require.ErrorIs(t, err, testErr)
   448  	})
   449  }
   450  
   451  func TestTopicReaderReconnectorFireReconnectOnRetryableError(t *testing.T) {
   452  	t.Run("Ok", func(t *testing.T) {
   453  		mc := gomock.NewController(t)
   454  		reconnector := &readerReconnector{
   455  			tracer: &trace.Topic{},
   456  		}
   457  
   458  		stream := NewMockbatchedStreamReader(mc)
   459  		reconnector.initChannelsAndClock()
   460  
   461  		reconnector.fireReconnectOnRetryableError(stream, nil)
   462  		select {
   463  		case <-reconnector.reconnectFromBadStream:
   464  			t.Fatal()
   465  		default:
   466  			// OK
   467  		}
   468  
   469  		reconnector.fireReconnectOnRetryableError(stream, xerrors.Wrap(errors.New("test")))
   470  		select {
   471  		case <-reconnector.reconnectFromBadStream:
   472  			t.Fatal()
   473  		default:
   474  			// OK
   475  		}
   476  
   477  		testErr := errors.New("test")
   478  		reconnector.fireReconnectOnRetryableError(stream, xerrors.Retryable(testErr))
   479  		res := <-reconnector.reconnectFromBadStream
   480  		require.Equal(t, stream, res.oldReader)
   481  		require.ErrorIs(t, res.reason, testErr)
   482  	})
   483  
   484  	t.Run("SkipWriteOnFullChannel", func(t *testing.T) {
   485  		mc := gomock.NewController(t)
   486  		defer mc.Finish()
   487  
   488  		reconnector := &readerReconnector{
   489  			tracer: &trace.Topic{},
   490  		}
   491  		stream := NewMockbatchedStreamReader(mc)
   492  		reconnector.initChannelsAndClock()
   493  
   494  	fillChannel:
   495  		for {
   496  			select {
   497  			case reconnector.reconnectFromBadStream <- newReconnectRequest(nil, nil):
   498  				// repeat
   499  			default:
   500  				break fillChannel
   501  			}
   502  		}
   503  
   504  		// write skipped
   505  		reconnector.fireReconnectOnRetryableError(stream, xerrors.Retryable(errors.New("test")))
   506  		res := <-reconnector.reconnectFromBadStream
   507  		require.Nil(t, res.oldReader)
   508  	})
   509  }
   510  
   511  func TestTopicReaderReconnectorReconnectWithError(t *testing.T) {
   512  	ctx := context.Background()
   513  	testErr := errors.New("test")
   514  	reconnector := &readerReconnector{
   515  		connectTimeout: value.InfiniteDuration,
   516  		readerConnect: func(ctx context.Context) (batchedStreamReader, error) {
   517  			return nil, testErr
   518  		},
   519  		streamErr: errors.New("start-error"),
   520  		tracer:    &trace.Topic{},
   521  	}
   522  	reconnector.initChannelsAndClock()
   523  	err := reconnector.reconnect(ctx, errors.New("test-reconnect"), nil)
   524  	require.ErrorIs(t, err, testErr)
   525  	require.ErrorIs(t, reconnector.streamErr, testErr)
   526  }
   527  
   528  type readerConnectFuncAnswer struct {
   529  	callback readerConnectFunc
   530  	stream   batchedStreamReader
   531  	err      error
   532  }
   533  
   534  func readerConnectFuncMock(answers ...readerConnectFuncAnswer) readerConnectFunc {
   535  	return func(ctx context.Context) (batchedStreamReader, error) {
   536  		res := answers[0]
   537  		if len(answers) > 1 {
   538  			answers = answers[1:]
   539  		}
   540  
   541  		if res.callback == nil {
   542  			return res.stream, res.err
   543  		}
   544  
   545  		return res.callback(ctx)
   546  	}
   547  }