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