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  )