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 }