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  )