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 }