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