github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/internal/table/retry_test.go (about) 1 package table 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "testing" 8 "time" 9 10 "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" 11 grpcCodes "google.golang.org/grpc/codes" 12 grpcStatus "google.golang.org/grpc/status" 13 14 "github.com/ydb-platform/ydb-go-sdk/v3/internal/table/config" 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/xrand" 18 "github.com/ydb-platform/ydb-go-sdk/v3/retry" 19 "github.com/ydb-platform/ydb-go-sdk/v3/table" 20 "github.com/ydb-platform/ydb-go-sdk/v3/testutil" 21 ) 22 23 func TestRetryerBackoffRetryCancelation(t *testing.T) { 24 for _, testErr := range []error{ 25 // Errors leading to Wait repeat. 26 xerrors.Transport( 27 grpcStatus.Error(grpcCodes.ResourceExhausted, ""), 28 ), 29 fmt.Errorf("wrap transport error: %w", xerrors.Transport( 30 grpcStatus.Error(grpcCodes.ResourceExhausted, ""), 31 )), 32 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_OVERLOADED)), 33 fmt.Errorf("wrap op error: %w", xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_OVERLOADED))), 34 } { 35 t.Run("", func(t *testing.T) { 36 backoff := make(chan chan time.Time) 37 p := SingleSession( 38 simpleSession(t), 39 ) 40 41 ctx, cancel := xcontext.WithCancel(context.Background()) 42 results := make(chan error) 43 go func() { 44 err := do(ctx, p, 45 config.New(), 46 func(ctx context.Context, _ table.Session) error { 47 return testErr 48 }, 49 nil, 50 retry.WithFastBackoff( 51 testutil.BackoffFunc(func(n int) <-chan time.Time { 52 ch := make(chan time.Time) 53 backoff <- ch 54 55 return ch 56 }), 57 ), 58 retry.WithSlowBackoff( 59 testutil.BackoffFunc(func(n int) <-chan time.Time { 60 ch := make(chan time.Time) 61 backoff <- ch 62 63 return ch 64 }), 65 ), 66 ) 67 results <- err 68 }() 69 70 select { 71 case <-backoff: 72 case res := <-results: 73 t.Fatalf("unexpected result: %v", res) 74 } 75 76 cancel() 77 }) 78 } 79 } 80 81 func TestRetryerBadSession(t *testing.T) { 82 closed := make(map[table.Session]bool) 83 p := SessionProviderFunc{ 84 OnGet: func(ctx context.Context) (*session, error) { 85 s := simpleSession(t) 86 s.onClose = append(s.onClose, func(s *session) { 87 closed[s] = true 88 }) 89 90 return s, nil 91 }, 92 OnPut: func(ctx context.Context, s *session) error { 93 if s.isClosing() { 94 return s.Close(ctx) 95 } 96 97 return nil 98 }, 99 } 100 var ( 101 i int 102 maxRetryes = 100 103 sessions []table.Session 104 ) 105 ctx, cancel := xcontext.WithCancel(context.Background()) 106 err := do(ctx, p, config.New(), 107 func(ctx context.Context, s table.Session) error { 108 sessions = append(sessions, s) 109 i++ 110 if i > maxRetryes { 111 cancel() 112 } 113 114 return xerrors.Operation( 115 xerrors.WithStatusCode(Ydb.StatusIds_BAD_SESSION), 116 ) 117 }, 118 func(err error) {}, 119 ) 120 if !xerrors.Is(err, context.Canceled) { 121 t.Errorf("unexpected error: %v", err) 122 } 123 seen := make(map[table.Session]bool, len(sessions)) 124 for _, s := range sessions { 125 if seen[s] { 126 t.Errorf("session used twice") 127 } else { 128 seen[s] = true 129 } 130 if !closed[s] { 131 t.Errorf("bad session was not closed") 132 } 133 } 134 } 135 136 func TestRetryerSessionClosing(t *testing.T) { 137 closed := make(map[table.Session]bool) 138 p := SessionProviderFunc{ 139 OnGet: func(ctx context.Context) (*session, error) { 140 s := simpleSession(t) 141 s.onClose = append(s.onClose, func(s *session) { 142 closed[s] = true 143 }) 144 145 return s, nil 146 }, 147 OnPut: func(ctx context.Context, s *session) error { 148 if s.isClosing() { 149 return s.Close(ctx) 150 } 151 152 return nil 153 }, 154 } 155 var sessions []table.Session 156 for i := 0; i < 1000; i++ { 157 err := do( 158 context.Background(), 159 p, 160 config.New(), 161 func(ctx context.Context, s table.Session) error { 162 sessions = append(sessions, s) 163 s.(*session).SetStatus(table.SessionClosing) 164 165 return nil 166 }, 167 nil, 168 ) 169 if err != nil { 170 t.Errorf("unexpected error: %v", err) 171 } 172 } 173 seen := make(map[table.Session]bool, len(sessions)) 174 for _, s := range sessions { 175 if seen[s] { 176 t.Errorf("session used twice") 177 } else { 178 seen[s] = true 179 } 180 if !closed[s] { 181 t.Errorf("bad session was not closed") 182 } 183 } 184 } 185 186 func TestRetryerImmediateReturn(t *testing.T) { 187 for _, testErr := range []error{ 188 xerrors.Operation( 189 xerrors.WithStatusCode(Ydb.StatusIds_GENERIC_ERROR), 190 ), 191 fmt.Errorf("wrap op error: %w", xerrors.Operation( 192 xerrors.WithStatusCode(Ydb.StatusIds_GENERIC_ERROR), 193 )), 194 xerrors.Transport( 195 grpcStatus.Error(grpcCodes.PermissionDenied, ""), 196 ), 197 fmt.Errorf("wrap transport error: %w", xerrors.Transport( 198 grpcStatus.Error(grpcCodes.PermissionDenied, ""), 199 )), 200 fmt.Errorf("whoa"), 201 } { 202 t.Run("", func(t *testing.T) { 203 defer func() { 204 if e := recover(); e != nil { 205 t.Fatalf("unexpected panic: %v", e) 206 } 207 }() 208 p := SingleSession( 209 simpleSession(t), 210 ) 211 err := do( 212 context.Background(), 213 p, 214 config.New(), 215 func(ctx context.Context, _ table.Session) error { 216 return testErr 217 }, 218 nil, 219 retry.WithFastBackoff( 220 testutil.BackoffFunc(func(n int) <-chan time.Time { 221 panic("this code will not be called") 222 }), 223 ), 224 retry.WithSlowBackoff( 225 testutil.BackoffFunc(func(n int) <-chan time.Time { 226 panic("this code will not be called") 227 }), 228 ), 229 ) 230 if !xerrors.Is(err, testErr) { 231 t.Fatalf("unexpected error: %v", err) 232 } 233 }) 234 } 235 } 236 237 // We are testing all suspentions of custom operation func against to all deadline 238 // timeouts - all sub-tests must have latency less than timeouts (+tolerance) 239 func TestRetryContextDeadline(t *testing.T) { 240 timeouts := []time.Duration{ 241 50 * time.Millisecond, 242 100 * time.Millisecond, 243 200 * time.Millisecond, 244 500 * time.Millisecond, 245 time.Second, 246 } 247 sleeps := []time.Duration{ 248 time.Nanosecond, 249 time.Microsecond, 250 time.Millisecond, 251 10 * time.Millisecond, 252 50 * time.Millisecond, 253 100 * time.Millisecond, 254 500 * time.Millisecond, 255 time.Second, 256 5 * time.Second, 257 } 258 errs := []error{ 259 io.EOF, 260 context.DeadlineExceeded, 261 fmt.Errorf("test error"), 262 xerrors.Transport( 263 grpcStatus.Error(grpcCodes.Canceled, ""), 264 ), 265 xerrors.Transport( 266 grpcStatus.Error(grpcCodes.Unknown, ""), 267 ), 268 xerrors.Transport( 269 grpcStatus.Error(grpcCodes.InvalidArgument, ""), 270 ), 271 xerrors.Transport( 272 grpcStatus.Error(grpcCodes.DeadlineExceeded, ""), 273 ), 274 xerrors.Transport( 275 grpcStatus.Error(grpcCodes.NotFound, ""), 276 ), 277 xerrors.Transport( 278 grpcStatus.Error(grpcCodes.AlreadyExists, ""), 279 ), 280 xerrors.Transport( 281 grpcStatus.Error(grpcCodes.PermissionDenied, ""), 282 ), 283 xerrors.Transport( 284 grpcStatus.Error(grpcCodes.ResourceExhausted, ""), 285 ), 286 xerrors.Transport( 287 grpcStatus.Error(grpcCodes.FailedPrecondition, ""), 288 ), 289 xerrors.Transport( 290 grpcStatus.Error(grpcCodes.Aborted, ""), 291 ), 292 xerrors.Transport( 293 grpcStatus.Error(grpcCodes.OutOfRange, ""), 294 ), 295 xerrors.Transport( 296 grpcStatus.Error(grpcCodes.Unimplemented, ""), 297 ), 298 xerrors.Transport( 299 grpcStatus.Error(grpcCodes.Internal, ""), 300 ), 301 xerrors.Transport( 302 grpcStatus.Error(grpcCodes.Unavailable, ""), 303 ), 304 xerrors.Transport( 305 grpcStatus.Error(grpcCodes.DataLoss, ""), 306 ), 307 xerrors.Transport( 308 grpcStatus.Error(grpcCodes.Unauthenticated, ""), 309 ), 310 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_STATUS_CODE_UNSPECIFIED)), 311 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST)), 312 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_UNAUTHORIZED)), 313 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_INTERNAL_ERROR)), 314 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_ABORTED)), 315 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_UNAVAILABLE)), 316 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_OVERLOADED)), 317 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_SCHEME_ERROR)), 318 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_GENERIC_ERROR)), 319 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_TIMEOUT)), 320 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_BAD_SESSION)), 321 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_PRECONDITION_FAILED)), 322 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_ALREADY_EXISTS)), 323 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_NOT_FOUND)), 324 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_SESSION_EXPIRED)), 325 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_CANCELLED)), 326 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_UNDETERMINED)), 327 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_UNSUPPORTED)), 328 xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_SESSION_BUSY)), 329 } 330 client := &Client{ 331 cc: testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{})), 332 } 333 p := SessionProviderFunc{ 334 OnGet: client.internalPoolCreateSession, 335 } 336 r := xrand.New(xrand.WithLock()) 337 for i := range timeouts { 338 for j := range sleeps { 339 timeout := timeouts[i] 340 sleep := sleeps[j] 341 t.Run(fmt.Sprintf("Timeout=%v,Sleep=%v", timeout, sleep), func(t *testing.T) { 342 ctx, cancel := xcontext.WithTimeout(context.Background(), timeout) 343 defer cancel() 344 _ = do( 345 ctx, 346 p, 347 config.New(), 348 func(ctx context.Context, _ table.Session) error { 349 select { 350 case <-ctx.Done(): 351 return ctx.Err() 352 case <-time.After(sleep): 353 return errs[r.Int(len(errs))] 354 } 355 }, 356 nil, 357 ) 358 }) 359 } 360 } 361 } 362 363 type CustomError struct { 364 Err error 365 } 366 367 func (e *CustomError) Error() string { 368 return fmt.Sprintf("custom error: %v", e.Err) 369 } 370 371 func (e *CustomError) Unwrap() error { 372 return e.Err 373 } 374 375 func TestRetryWithCustomErrors(t *testing.T) { 376 var ( 377 limit = 10 378 ctx = context.Background() 379 p = SessionProviderFunc{ 380 OnGet: func(ctx context.Context) (*session, error) { 381 return simpleSession(t), nil 382 }, 383 } 384 ) 385 for _, test := range []struct { 386 error error 387 retriable bool 388 deleteSession bool 389 }{ 390 { 391 error: &CustomError{ 392 Err: retry.RetryableError( 393 fmt.Errorf("custom error"), 394 retry.WithDeleteSession(), 395 ), 396 }, 397 retriable: true, 398 deleteSession: true, 399 }, 400 { 401 error: &CustomError{ 402 Err: xerrors.Operation( 403 xerrors.WithStatusCode( 404 Ydb.StatusIds_BAD_SESSION, 405 ), 406 ), 407 }, 408 retriable: true, 409 deleteSession: true, 410 }, 411 { 412 error: &CustomError{ 413 Err: fmt.Errorf( 414 "wrapped error: %w", 415 xerrors.Operation( 416 xerrors.WithStatusCode( 417 Ydb.StatusIds_BAD_SESSION, 418 ), 419 ), 420 ), 421 }, 422 retriable: true, 423 deleteSession: true, 424 }, 425 { 426 error: &CustomError{ 427 Err: fmt.Errorf( 428 "wrapped error: %w", 429 xerrors.Operation( 430 xerrors.WithStatusCode( 431 Ydb.StatusIds_UNAUTHORIZED, 432 ), 433 ), 434 ), 435 }, 436 retriable: false, 437 deleteSession: false, 438 }, 439 } { 440 t.Run(test.error.Error(), func(t *testing.T) { 441 var ( 442 i = 0 443 sessions = make(map[table.Session]int) 444 ) 445 err := do( 446 ctx, 447 p, 448 config.New(), 449 func(ctx context.Context, s table.Session) (err error) { 450 sessions[s]++ 451 i++ 452 if i < limit { 453 return test.error 454 } 455 456 return nil 457 }, 458 nil, 459 ) 460 //nolint:nestif 461 if test.retriable { 462 if i != limit { 463 t.Fatalf("unexpected i: %d, err: %v", i, err) 464 } 465 if test.deleteSession { 466 if len(sessions) != limit { 467 t.Fatalf("unexpected len(sessions): %d, err: %v", len(sessions), err) 468 } 469 for s, n := range sessions { 470 if n != 1 { 471 t.Fatalf("unexpected session usage: %d, session: %v", n, s.ID()) 472 } 473 } 474 } 475 } else { 476 if i != 1 { 477 t.Fatalf("unexpected i: %d, err: %v", i, err) 478 } 479 if len(sessions) != 1 { 480 t.Fatalf("unexpected len(sessions): %d, err: %v", len(sessions), err) 481 } 482 } 483 }) 484 } 485 } 486 487 type SessionProviderFunc struct { 488 OnGet func(context.Context) (*session, error) 489 OnPut func(context.Context, *session) error 490 } 491 492 var _ SessionProvider = SessionProviderFunc{} 493 494 func (f SessionProviderFunc) Get(ctx context.Context) (*session, error) { 495 if f.OnGet == nil { 496 return nil, xerrors.WithStackTrace(errNoSession) 497 } 498 499 return f.OnGet(ctx) 500 } 501 502 func (f SessionProviderFunc) Put(ctx context.Context, s *session) error { 503 if f.OnPut == nil { 504 return xerrors.WithStackTrace(testutil.ErrNotImplemented) 505 } 506 507 return f.OnPut(ctx, s) 508 } 509 510 // SingleSession returns SessionProvider that uses only given session during 511 // retries. 512 func SingleSession(s *session) SessionProvider { 513 return &singleSession{s: s} 514 } 515 516 type singleSession struct { 517 s *session 518 empty bool 519 } 520 521 func (s *singleSession) Get(context.Context) (*session, error) { 522 if s.empty { 523 return nil, xerrors.WithStackTrace(errNoSession) 524 } 525 s.empty = true 526 527 return s.s, nil 528 } 529 530 func (s *singleSession) Put(_ context.Context, x *session) error { 531 if x != s.s { 532 return xerrors.WithStackTrace(errUnexpectedSession) 533 } 534 if !s.empty { 535 return xerrors.WithStackTrace(errSessionOverflow) 536 } 537 s.empty = false 538 539 return nil 540 } 541 542 var ( 543 errNoSession = xerrors.Wrap(fmt.Errorf("no session")) 544 errUnexpectedSession = xerrors.Wrap(fmt.Errorf("unexpected session")) 545 errSessionOverflow = xerrors.Wrap(fmt.Errorf("session overflow")) 546 )