github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/pool/pool_test.go (about)

     1  package pool
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"path"
     9  	"runtime"
    10  	"runtime/debug"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/jonboulle/clockwork"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb"
    19  	grpcCodes "google.golang.org/grpc/codes"
    20  	grpcStatus "google.golang.org/grpc/status"
    21  
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/closer"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
    24  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    26  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xrand"
    27  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    28  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    29  	"github.com/ydb-platform/ydb-go-sdk/v3/retry"
    30  	"github.com/ydb-platform/ydb-go-sdk/v3/testutil"
    31  )
    32  
    33  type (
    34  	testItem struct {
    35  		v uint32
    36  
    37  		closed bytes.Buffer
    38  
    39  		onClose   func() error
    40  		onIsAlive func() bool
    41  	}
    42  	testWaitChPool struct {
    43  		xsync.Pool[chan *testItem]
    44  		testHookGetWaitCh func()
    45  	}
    46  )
    47  
    48  var defaultTrace = &Trace{
    49  	OnNew: func(ctx *context.Context, call stack.Caller) func(limit int) {
    50  		return func(limit int) {
    51  		}
    52  	},
    53  	OnClose: func(ctx *context.Context, call stack.Caller) func(err error) {
    54  		return func(err error) {
    55  		}
    56  	},
    57  	OnTry: func(ctx *context.Context, call stack.Caller) func(err error) {
    58  		return func(err error) {
    59  		}
    60  	},
    61  	OnWith: func(ctx *context.Context, call stack.Caller) func(attempts int, err error) {
    62  		return func(attempts int, err error) {
    63  		}
    64  	},
    65  	OnPut: func(ctx *context.Context, call stack.Caller, item any) func(err error) {
    66  		return func(err error) {
    67  		}
    68  	},
    69  	OnGet: func(ctx *context.Context, call stack.Caller) func(item any, attempts int, err error) {
    70  		return func(item any, attempts int, err error) {
    71  		}
    72  	},
    73  	onWait: func() func(item any, err error) {
    74  		return func(item any, err error) {
    75  		}
    76  	},
    77  	OnChange: func(stats Stats) {
    78  	},
    79  }
    80  
    81  func (p *testWaitChPool) GetOrNew() *chan *testItem {
    82  	if p.testHookGetWaitCh != nil {
    83  		p.testHookGetWaitCh()
    84  	}
    85  
    86  	return p.Pool.GetOrNew()
    87  }
    88  
    89  func (p *testWaitChPool) whenWantWaitCh() <-chan struct{} {
    90  	var (
    91  		prev = p.testHookGetWaitCh
    92  		ch   = make(chan struct{})
    93  	)
    94  	p.testHookGetWaitCh = func() {
    95  		p.testHookGetWaitCh = prev
    96  		close(ch)
    97  	}
    98  
    99  	return ch
   100  }
   101  
   102  func (p *testWaitChPool) Put(ch *chan *testItem) {}
   103  
   104  func (t *testItem) IsAlive() bool {
   105  	if t.onIsAlive != nil {
   106  		return t.onIsAlive()
   107  	}
   108  
   109  	return true
   110  }
   111  
   112  func (t *testItem) ID() string {
   113  	return ""
   114  }
   115  
   116  func (t *testItem) Close(context.Context) error {
   117  	if t.closed.Len() > 0 {
   118  		debug.PrintStack()
   119  		fmt.Println(t.closed.String())
   120  		panic("item already closed")
   121  	}
   122  
   123  	t.closed.Write(debug.Stack())
   124  
   125  	if t.onClose != nil {
   126  		return t.onClose()
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  func caller() string {
   133  	_, file, line, _ := runtime.Caller(2)
   134  
   135  	return fmt.Sprintf("%s:%d", path.Base(file), line)
   136  }
   137  
   138  func mustGetItem[PT ItemConstraint[T], T any](t testing.TB, p *Pool[PT, T]) PT {
   139  	s, err := p.getItem(context.Background())
   140  	if err != nil {
   141  		t.Helper()
   142  		t.Fatalf("%s: %v", caller(), err)
   143  	}
   144  
   145  	return s
   146  }
   147  
   148  func mustPutItem[PT ItemConstraint[T], T any](t testing.TB, p *Pool[PT, T], item PT) {
   149  	if err := p.putItem(context.Background(), item); err != nil {
   150  		t.Helper()
   151  		t.Fatalf("%s: %v", caller(), err)
   152  	}
   153  }
   154  
   155  func mustClose(t testing.TB, pool closer.Closer) {
   156  	if err := pool.Close(context.Background()); err != nil {
   157  		t.Helper()
   158  		t.Fatalf("%s: %v", caller(), err)
   159  	}
   160  }
   161  
   162  func TestPool(t *testing.T) { //nolint:gocyclo
   163  	rootCtx := xtest.Context(t)
   164  	t.Run("New", func(t *testing.T) {
   165  		t.Run("Default", func(t *testing.T) {
   166  			p := New[*testItem, testItem](rootCtx,
   167  				WithTrace[*testItem, testItem](defaultTrace),
   168  			)
   169  			err := p.With(rootCtx, func(ctx context.Context, testItem *testItem) error {
   170  				return nil
   171  			})
   172  			require.NoError(t, err)
   173  		})
   174  		t.Run("WithLimit", func(t *testing.T) {
   175  			p := New[*testItem, testItem](rootCtx, WithLimit[*testItem, testItem](1),
   176  				WithTrace[*testItem, testItem](defaultTrace),
   177  			)
   178  			require.EqualValues(t, 1, p.config.limit)
   179  		})
   180  		t.Run("WithItemUsageLimit", func(t *testing.T) {
   181  			var newCounter int64
   182  			p := New[*testItem, testItem](rootCtx,
   183  				WithLimit[*testItem, testItem](1),
   184  				WithItemUsageLimit[*testItem, testItem](5),
   185  				WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   186  				WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   187  				WithCreateItemFunc(func(context.Context) (*testItem, error) {
   188  					atomic.AddInt64(&newCounter, 1)
   189  
   190  					var v testItem
   191  
   192  					return &v, nil
   193  				}),
   194  			)
   195  			require.EqualValues(t, 1, p.config.limit)
   196  			var lambdaCounter int64
   197  			err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   198  				if atomic.AddInt64(&lambdaCounter, 1) < 10 {
   199  					return xerrors.Retryable(errors.New("test"))
   200  				}
   201  
   202  				return nil
   203  			})
   204  			require.NoError(t, err)
   205  			require.EqualValues(t, 2, newCounter)
   206  		})
   207  		t.Run("WithCreateItemFunc", func(t *testing.T) {
   208  			var newCounter int64
   209  			p := New(rootCtx,
   210  				WithLimit[*testItem, testItem](1),
   211  				WithCreateItemFunc(func(context.Context) (*testItem, error) {
   212  					atomic.AddInt64(&newCounter, 1)
   213  					var v testItem
   214  
   215  					return &v, nil
   216  				}),
   217  				WithTrace[*testItem, testItem](defaultTrace),
   218  			)
   219  			err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   220  				return nil
   221  			})
   222  			require.NoError(t, err)
   223  			require.EqualValues(t, p.config.limit, atomic.LoadInt64(&newCounter))
   224  		})
   225  	})
   226  	t.Run("Close", func(t *testing.T) {
   227  		counter := 0
   228  		xtest.TestManyTimes(t, func(t testing.TB) {
   229  			counter++
   230  			defer func() {
   231  				if counter%1000 == 0 {
   232  					t.Logf("%d times test passed", counter)
   233  				}
   234  			}()
   235  
   236  			var (
   237  				created atomic.Int32
   238  				closed  = [...]bool{false, false, false}
   239  			)
   240  
   241  			p := New[*testItem, testItem](rootCtx,
   242  				WithLimit[*testItem, testItem](3),
   243  				WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   244  				WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   245  				WithCreateItemFunc(func(context.Context) (*testItem, error) {
   246  					var (
   247  						idx = created.Add(1) - 1
   248  						v   = testItem{
   249  							v: 0,
   250  							onClose: func() error {
   251  								closed[idx] = true
   252  
   253  								return nil
   254  							},
   255  						}
   256  					)
   257  
   258  					return &v, nil
   259  				}),
   260  				// replace default async closer for sync testing
   261  				WithSyncCloseItem[*testItem, testItem](),
   262  				WithTrace[*testItem, testItem](defaultTrace),
   263  			)
   264  
   265  			defer func() {
   266  				_ = p.Close(context.Background())
   267  			}()
   268  
   269  			require.Empty(t, p.index)
   270  			require.Zero(t, p.idle.Len())
   271  
   272  			var (
   273  				s1 = mustGetItem(t, p)
   274  				s2 = mustGetItem(t, p)
   275  				s3 = mustGetItem(t, p)
   276  			)
   277  
   278  			require.Len(t, p.index, 3)
   279  			require.Zero(t, p.idle.Len())
   280  
   281  			mustPutItem(t, p, s1)
   282  			mustPutItem(t, p, s2)
   283  
   284  			require.Len(t, p.index, 3)
   285  			require.Equal(t, 2, p.idle.Len())
   286  
   287  			mustClose(t, p)
   288  
   289  			require.Len(t, p.index, 1)
   290  			require.Zero(t, p.idle.Len())
   291  
   292  			require.True(t, closed[0])  // idle item in pool
   293  			require.True(t, closed[1])  // idle item in pool
   294  			require.False(t, closed[2]) // item extracted from idle but closed later on putItem
   295  
   296  			require.ErrorIs(t, p.putItem(context.Background(), s3), errClosedPool)
   297  
   298  			require.True(t, closed[2]) // after putItem s3 must be closed
   299  		})
   300  		t.Run("WhenWaiting", func(t *testing.T) {
   301  			for _, test := range []struct {
   302  				name string
   303  				racy bool
   304  			}{
   305  				{
   306  					name: "normal",
   307  					racy: false,
   308  				},
   309  				{
   310  					name: "racy",
   311  					racy: true,
   312  				},
   313  			} {
   314  				t.Run(test.name, func(t *testing.T) {
   315  					var (
   316  						get  = make(chan struct{})
   317  						wait = make(chan struct{})
   318  						got  = make(chan error)
   319  					)
   320  					waitChPool := &testWaitChPool{
   321  						Pool: xsync.Pool[chan *testItem]{
   322  							New: func() *chan *testItem {
   323  								ch := make(chan *testItem)
   324  
   325  								return &ch
   326  							},
   327  						},
   328  					}
   329  					p := New[*testItem, testItem](rootCtx,
   330  						// replace default async closer for sync testing
   331  						WithSyncCloseItem[*testItem, testItem](),
   332  						WithLimit[*testItem, testItem](1),
   333  						WithTrace[*testItem, testItem](&Trace{
   334  							onWait: func() func(item any, err error) {
   335  								wait <- struct{}{}
   336  
   337  								return nil
   338  							},
   339  						}),
   340  					)
   341  					p.waitChPool = waitChPool
   342  					defer func() {
   343  						_ = p.Close(context.Background())
   344  					}()
   345  
   346  					// first call getItem creates an item and store in index
   347  					// second call getItem from pool with limit === 1 will skip
   348  					// create item step (because pool have not enough space for
   349  					// creating new items) and will freeze until wait free item from pool
   350  					mustGetItem(t, p)
   351  
   352  					go func() {
   353  						p.config.trace.OnGet = func(ctx *context.Context, call stack.Caller) func(item any, attempts int, err error) {
   354  							get <- struct{}{}
   355  
   356  							return nil
   357  						}
   358  
   359  						_, err := p.getItem(context.Background())
   360  						got <- err
   361  					}()
   362  
   363  					regWait := waitChPool.whenWantWaitCh()
   364  					<-get     // Await for getter blocked on awaiting session.
   365  					<-regWait // Let the getter register itself in the wait queue.
   366  
   367  					if test.racy {
   368  						// We are testing the case, when session consumer registered
   369  						// himself in the wait queue, but not ready to receive the
   370  						// session when session arrives (that is, stuck between
   371  						// pushing channel in the list and reading from the channel).
   372  						_ = p.Close(context.Background())
   373  						<-wait
   374  					} else {
   375  						// We are testing the normal case, when session consumer registered
   376  						// himself in the wait queue and successfully blocked on
   377  						// reading from signaling channel.
   378  						<-wait
   379  						// Let the waiting goroutine to block on reading from channel.
   380  						_ = p.Close(context.Background())
   381  					}
   382  
   383  					const timeout = time.Second
   384  					select {
   385  					case err := <-got:
   386  						if !xerrors.Is(err, errClosedPool) {
   387  							t.Fatalf(
   388  								"unexpected error: %q; want %q'",
   389  								err, errClosedPool,
   390  							)
   391  						}
   392  					case <-p.config.clock.After(timeout):
   393  						t.Fatalf("no result after %s", timeout)
   394  					}
   395  				})
   396  			}
   397  		})
   398  		t.Run("IdleSessions", func(t *testing.T) {
   399  			xtest.TestManyTimes(t, func(t testing.TB) {
   400  				var (
   401  					idleThreshold = 4 * time.Second
   402  					closedCount   atomic.Int64
   403  					fakeClock     = clockwork.NewFakeClock()
   404  				)
   405  				p := New[*testItem, testItem](rootCtx,
   406  					WithLimit[*testItem, testItem](2),
   407  					WithCreateItemTimeout[*testItem, testItem](0),
   408  					WithCreateItemFunc[*testItem, testItem](func(ctx context.Context) (*testItem, error) {
   409  						v := testItem{
   410  							v: 0,
   411  							onClose: func() error {
   412  								closedCount.Add(1)
   413  
   414  								return nil
   415  							},
   416  						}
   417  
   418  						return &v, nil
   419  					}),
   420  					WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   421  					// replace default async closer for sync testing
   422  					WithSyncCloseItem[*testItem, testItem](),
   423  					WithClock[*testItem, testItem](fakeClock),
   424  					WithIdleTimeToLive[*testItem, testItem](idleThreshold),
   425  					WithTrace[*testItem, testItem](defaultTrace),
   426  				)
   427  
   428  				s1 := mustGetItem(t, p)
   429  				s2 := mustGetItem(t, p)
   430  
   431  				// Put both items at the absolutely same time.
   432  				// That is, both items must be updated their lastUsage timestamp.
   433  				mustPutItem(t, p, s1)
   434  				mustPutItem(t, p, s2)
   435  
   436  				require.Len(t, p.index, 2)
   437  				require.Equal(t, 2, p.idle.Len())
   438  
   439  				// Move clock to longer than idleTimeToLive
   440  				fakeClock.Advance(idleThreshold + time.Nanosecond)
   441  
   442  				// on get item from idle list the pool must check the item idle timestamp
   443  				// both existing items must be closed
   444  				// getItem must create a new item and return it from getItem
   445  				s3 := mustGetItem(t, p)
   446  
   447  				require.Len(t, p.index, 1)
   448  
   449  				if !closedCount.CompareAndSwap(2, 0) {
   450  					t.Fatal("unexpected number of closed items")
   451  				}
   452  
   453  				// Move time to idleTimeToLive / 2 - this emulate a "spent" some time working within item.
   454  				fakeClock.Advance(idleThreshold / 2)
   455  
   456  				// Now put that item back
   457  				// pool must update a lastUsage timestamp of item
   458  				mustPutItem(t, p, s3)
   459  
   460  				// Move time to idleTimeToLive / 2
   461  				// Total time since last updating lastUsage timestampe is more than idleTimeToLive
   462  				fakeClock.Advance(idleThreshold/2 + time.Nanosecond)
   463  
   464  				require.Len(t, p.index, 1)
   465  				require.Equal(t, 1, p.idle.Len())
   466  
   467  				s4 := mustGetItem(t, p)
   468  				require.Equal(t, s3, s4)
   469  				require.Len(t, p.index, 1)
   470  				require.Equal(t, 0, p.idle.Len())
   471  				mustPutItem(t, p, s4)
   472  
   473  				_ = p.Close(context.Background())
   474  
   475  				require.Empty(t, p.index)
   476  				require.Equal(t, 0, p.idle.Len())
   477  			}, xtest.StopAfter(3*time.Second))
   478  		})
   479  	})
   480  	t.Run("Retry", func(t *testing.T) {
   481  		t.Run("CreateItem", func(t *testing.T) {
   482  			t.Run("context", func(t *testing.T) {
   483  				t.Run("Cancelled", func(t *testing.T) {
   484  					var counter int64
   485  					p := New(rootCtx,
   486  						WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   487  						WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   488  						WithCreateItemFunc(func(context.Context) (*testItem, error) {
   489  							atomic.AddInt64(&counter, 1)
   490  
   491  							if atomic.LoadInt64(&counter) < 10 {
   492  								return nil, context.Canceled
   493  							}
   494  
   495  							var v testItem
   496  
   497  							return &v, nil
   498  						}),
   499  					)
   500  					err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   501  						return nil
   502  					})
   503  					require.NoError(t, err)
   504  					require.GreaterOrEqual(t, atomic.LoadInt64(&counter), int64(10))
   505  				})
   506  				t.Run("DeadlineExceeded", func(t *testing.T) {
   507  					var counter int64
   508  					p := New(rootCtx,
   509  						WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   510  						WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   511  						WithCreateItemFunc(func(context.Context) (*testItem, error) {
   512  							atomic.AddInt64(&counter, 1)
   513  
   514  							if atomic.LoadInt64(&counter) < 10 {
   515  								return nil, context.DeadlineExceeded
   516  							}
   517  
   518  							var v testItem
   519  
   520  							return &v, nil
   521  						}),
   522  					)
   523  					err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   524  						return nil
   525  					})
   526  					require.NoError(t, err)
   527  					require.GreaterOrEqual(t, atomic.LoadInt64(&counter), int64(10))
   528  				})
   529  			})
   530  			t.Run("OnTransportError", func(t *testing.T) {
   531  				var counter int64
   532  				p := New(rootCtx,
   533  					WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   534  					WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   535  					WithCreateItemFunc(func(context.Context) (*testItem, error) {
   536  						atomic.AddInt64(&counter, 1)
   537  
   538  						if atomic.LoadInt64(&counter) < 10 {
   539  							return nil, xerrors.Transport(grpcStatus.Error(grpcCodes.Unavailable, ""))
   540  						}
   541  
   542  						var v testItem
   543  
   544  						return &v, nil
   545  					}),
   546  				)
   547  				err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   548  					return nil
   549  				})
   550  				require.NoError(t, err)
   551  				require.GreaterOrEqual(t, atomic.LoadInt64(&counter), int64(10))
   552  			})
   553  			t.Run("OnOperationError", func(t *testing.T) {
   554  				var counter int64
   555  				p := New(rootCtx,
   556  					WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   557  					WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   558  					WithCreateItemFunc(func(context.Context) (*testItem, error) {
   559  						atomic.AddInt64(&counter, 1)
   560  
   561  						if atomic.LoadInt64(&counter) < 10 {
   562  							return nil, xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_UNAVAILABLE))
   563  						}
   564  
   565  						var v testItem
   566  
   567  						return &v, nil
   568  					}),
   569  				)
   570  				err := p.With(rootCtx, func(ctx context.Context, item *testItem) error {
   571  					return nil
   572  				})
   573  				require.NoError(t, err)
   574  				require.GreaterOrEqual(t, atomic.LoadInt64(&counter), int64(10))
   575  			})
   576  			t.Run("NilNil", func(t *testing.T) {
   577  				xtest.TestManyTimes(t, func(t testing.TB) {
   578  					limit := 100
   579  					ctx, cancel := xcontext.WithTimeout(
   580  						context.Background(),
   581  						55*time.Second,
   582  					)
   583  					defer cancel()
   584  					p := New[*testItem, testItem](rootCtx,
   585  						// replace default async closer for sync testing
   586  						WithSyncCloseItem[*testItem, testItem](),
   587  					)
   588  					defer func() {
   589  						_ = p.Close(context.Background())
   590  					}()
   591  					r := xrand.New(xrand.WithLock())
   592  					errCh := make(chan error, limit*10)
   593  					fn := func(wg *sync.WaitGroup) {
   594  						defer wg.Done()
   595  						childCtx, childCancel := xcontext.WithTimeout(
   596  							ctx,
   597  							time.Duration(r.Int64(int64(time.Second))),
   598  						)
   599  						defer childCancel()
   600  						s, err := p.createItem(childCtx)
   601  						if s == nil && err == nil {
   602  							errCh <- fmt.Errorf("unexpected result: <%v, %w>", s, err)
   603  						}
   604  					}
   605  					wg := &sync.WaitGroup{}
   606  					wg.Add(limit * 10)
   607  					for i := 0; i < limit*10; i++ {
   608  						go fn(wg)
   609  					}
   610  					go func() {
   611  						wg.Wait()
   612  						close(errCh)
   613  					}()
   614  					for e := range errCh {
   615  						t.Fatal(e)
   616  					}
   617  				})
   618  			})
   619  		})
   620  		t.Run("On", func(t *testing.T) {
   621  			t.Run("Context", func(t *testing.T) {
   622  				t.Run("Canceled", func(t *testing.T) {
   623  					ctx, cancel := context.WithCancel(rootCtx)
   624  					cancel()
   625  					p := New[*testItem, testItem](ctx, WithLimit[*testItem, testItem](1))
   626  					err := p.With(ctx, func(ctx context.Context, testItem *testItem) error {
   627  						return nil
   628  					})
   629  					require.ErrorIs(t, err, context.Canceled)
   630  				})
   631  				t.Run("DeadlineExceeded", func(t *testing.T) {
   632  					ctx, cancel := context.WithTimeout(rootCtx, 0)
   633  					cancel()
   634  					p := New[*testItem, testItem](ctx, WithLimit[*testItem, testItem](1))
   635  					err := p.With(ctx, func(ctx context.Context, testItem *testItem) error {
   636  						return nil
   637  					})
   638  					require.ErrorIs(t, err, context.DeadlineExceeded)
   639  				})
   640  			})
   641  		})
   642  		t.Run("DoBackoffRetryCancelation", func(t *testing.T) {
   643  			for _, testErr := range []error{
   644  				// Errors leading to Wait repeat.
   645  				xerrors.Transport(
   646  					grpcStatus.Error(grpcCodes.ResourceExhausted, ""),
   647  				),
   648  				fmt.Errorf("wrap transport error: %w", xerrors.Transport(
   649  					grpcStatus.Error(grpcCodes.ResourceExhausted, ""),
   650  				)),
   651  				xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_OVERLOADED)),
   652  				fmt.Errorf("wrap op error: %w", xerrors.Operation(xerrors.WithStatusCode(Ydb.StatusIds_OVERLOADED))),
   653  			} {
   654  				t.Run("", func(t *testing.T) {
   655  					backoff := make(chan chan time.Time)
   656  					ctx, cancel := xcontext.WithCancel(context.Background())
   657  					p := New[*testItem, testItem](ctx, WithLimit[*testItem, testItem](1))
   658  
   659  					results := make(chan error)
   660  					go func() {
   661  						err := p.With(ctx,
   662  							func(ctx context.Context, item *testItem) error {
   663  								return testErr
   664  							},
   665  							retry.WithFastBackoff(
   666  								testutil.BackoffFunc(func(n int) <-chan time.Time {
   667  									ch := make(chan time.Time)
   668  									backoff <- ch
   669  
   670  									return ch
   671  								}),
   672  							),
   673  							retry.WithSlowBackoff(
   674  								testutil.BackoffFunc(func(n int) <-chan time.Time {
   675  									ch := make(chan time.Time)
   676  									backoff <- ch
   677  
   678  									return ch
   679  								}),
   680  							),
   681  						)
   682  						results <- err
   683  					}()
   684  
   685  					select {
   686  					case <-backoff:
   687  						t.Logf("expected result")
   688  					case res := <-results:
   689  						t.Fatalf("unexpected result: %v", res)
   690  					}
   691  
   692  					cancel()
   693  				})
   694  			}
   695  		})
   696  	})
   697  	t.Run("Item", func(t *testing.T) {
   698  		t.Run("Close", func(t *testing.T) {
   699  			xtest.TestManyTimes(t, func(t testing.TB) {
   700  				var (
   701  					createCounter int64
   702  					closeCounter  int64
   703  				)
   704  				p := New(rootCtx,
   705  					WithLimit[*testItem, testItem](1),
   706  					WithCreateItemFunc(func(context.Context) (*testItem, error) {
   707  						atomic.AddInt64(&createCounter, 1)
   708  
   709  						v := &testItem{
   710  							onClose: func() error {
   711  								atomic.AddInt64(&closeCounter, 1)
   712  
   713  								return nil
   714  							},
   715  						}
   716  
   717  						return v, nil
   718  					}),
   719  					// replace default async closer for sync testing
   720  					WithSyncCloseItem[*testItem, testItem](),
   721  				)
   722  				err := p.With(rootCtx, func(ctx context.Context, testItem *testItem) error {
   723  					return nil
   724  				})
   725  				require.NoError(t, err)
   726  				require.GreaterOrEqual(t, atomic.LoadInt64(&createCounter), atomic.LoadInt64(&closeCounter))
   727  				err = p.Close(rootCtx)
   728  				require.NoError(t, err)
   729  				require.EqualValues(t, atomic.LoadInt64(&createCounter), atomic.LoadInt64(&closeCounter))
   730  			})
   731  		})
   732  		t.Run("IsAlive", func(t *testing.T) {
   733  			xtest.TestManyTimes(t, func(t testing.TB) {
   734  				var (
   735  					newItems    atomic.Int64
   736  					deleteItems atomic.Int64
   737  					expErr      = xerrors.Retryable(errors.New("expected error"), xerrors.InvalidObject())
   738  				)
   739  				p := New(rootCtx,
   740  					WithLimit[*testItem, testItem](1),
   741  					WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   742  					WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   743  					WithCreateItemFunc(func(context.Context) (*testItem, error) {
   744  						newItems.Add(1)
   745  
   746  						v := &testItem{
   747  							onClose: func() error {
   748  								deleteItems.Add(1)
   749  
   750  								return nil
   751  							},
   752  							onIsAlive: func() bool {
   753  								return newItems.Load() >= 10
   754  							},
   755  						}
   756  
   757  						return v, nil
   758  					}),
   759  					// replace default async closer for sync testing
   760  					WithSyncCloseItem[*testItem, testItem](),
   761  				)
   762  				err := p.With(rootCtx, func(ctx context.Context, testItem *testItem) error {
   763  					if newItems.Load() < 10 {
   764  						return expErr
   765  					}
   766  
   767  					return nil
   768  				})
   769  				require.NoError(t, err)
   770  				require.GreaterOrEqual(t, newItems.Load(), int64(9))
   771  				require.GreaterOrEqual(t, newItems.Load(), deleteItems.Load())
   772  				err = p.Close(rootCtx)
   773  				require.NoError(t, err)
   774  				require.EqualValues(t, newItems.Load(), deleteItems.Load())
   775  			}, xtest.StopAfter(3*time.Second))
   776  		})
   777  	})
   778  	t.Run("With", func(t *testing.T) {
   779  		t.Run("ExplicitSessionClose", func(t *testing.T) {
   780  			var (
   781  				created atomic.Int32
   782  				closed  atomic.Int32
   783  			)
   784  			assertCreated := func(exp int32) {
   785  				if act := created.Load(); act != exp {
   786  					t.Errorf(
   787  						"unexpected number of created items: %v; want %v",
   788  						act, exp,
   789  					)
   790  				}
   791  			}
   792  			assertClosed := func(exp int32) {
   793  				if act := closed.Load(); act != exp {
   794  					t.Errorf(
   795  						"unexpected number of closed items: %v; want %v",
   796  						act, exp,
   797  					)
   798  				}
   799  			}
   800  			p := New[*testItem, testItem](rootCtx,
   801  				WithLimit[*testItem, testItem](1),
   802  				WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   803  				WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   804  				WithCreateItemFunc(func(context.Context) (*testItem, error) {
   805  					created.Add(1)
   806  					v := testItem{
   807  						v: 0,
   808  						onClose: func() error {
   809  							closed.Add(1)
   810  
   811  							return nil
   812  						},
   813  					}
   814  
   815  					return &v, nil
   816  				}),
   817  				// replace default async closer for sync testing
   818  				WithSyncCloseItem[*testItem, testItem](),
   819  			)
   820  			defer func() {
   821  				_ = p.Close(context.Background())
   822  			}()
   823  
   824  			s := mustGetItem(t, p)
   825  			assertCreated(1)
   826  
   827  			mustPutItem(t, p, s)
   828  			assertClosed(0)
   829  
   830  			mustGetItem(t, p)
   831  			assertCreated(1)
   832  
   833  			p.closeItem(context.Background(), s)
   834  			delete(p.index, s)
   835  			assertClosed(1)
   836  
   837  			mustGetItem(t, p)
   838  			assertCreated(2)
   839  		})
   840  		t.Run("Racy", func(t *testing.T) {
   841  			xtest.TestManyTimes(t, func(t testing.TB) {
   842  				trace := &Trace{
   843  					OnChange: func(stats Stats) {
   844  						require.GreaterOrEqual(t, stats.Limit, stats.Idle)
   845  					},
   846  				}
   847  				p := New[*testItem, testItem](rootCtx,
   848  					WithTrace[*testItem, testItem](trace),
   849  					// replace default async closer for sync testing
   850  					WithSyncCloseItem[*testItem, testItem](),
   851  				)
   852  				r := xrand.New(xrand.WithLock())
   853  				var wg sync.WaitGroup
   854  				wg.Add(DefaultLimit*2 + 1)
   855  				for range make([]struct{}, DefaultLimit*2) {
   856  					go func() {
   857  						defer wg.Done()
   858  						childCtx, childCancel := xcontext.WithTimeout(
   859  							rootCtx,
   860  							time.Duration(r.Int64(int64(time.Second))),
   861  						)
   862  						defer childCancel()
   863  						err := p.With(childCtx, func(ctx context.Context, testItem *testItem) error {
   864  							return nil
   865  						})
   866  						if err != nil && !xerrors.Is(err, errClosedPool, context.Canceled) {
   867  							t.Failed()
   868  						}
   869  					}()
   870  				}
   871  				go func() {
   872  					defer wg.Done()
   873  					time.Sleep(time.Millisecond)
   874  					err := p.Close(rootCtx)
   875  					require.NoError(t, err)
   876  				}()
   877  				wg.Wait()
   878  			})
   879  		})
   880  		t.Run("ParallelCreation", func(t *testing.T) {
   881  			xtest.TestManyTimes(t, func(t testing.TB) {
   882  				trace := &Trace{
   883  					OnChange: func(stats Stats) {
   884  						require.Equal(t, DefaultLimit, stats.Limit)
   885  						require.LessOrEqual(t, stats.Idle, DefaultLimit)
   886  					},
   887  				}
   888  				p := New[*testItem, testItem](rootCtx,
   889  					WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   890  					WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   891  					WithTrace[*testItem, testItem](trace),
   892  				)
   893  				var wg sync.WaitGroup
   894  				for range make([]struct{}, DefaultLimit*10) {
   895  					wg.Add(1)
   896  					go func() {
   897  						defer wg.Done()
   898  						err := p.With(rootCtx, func(ctx context.Context, testItem *testItem) error {
   899  							return nil
   900  						})
   901  						if err != nil && !xerrors.Is(err, errClosedPool, context.Canceled) {
   902  							t.Failed()
   903  						}
   904  						stats := p.Stats()
   905  						require.LessOrEqual(t, stats.Idle, DefaultLimit)
   906  					}()
   907  				}
   908  
   909  				wg.Wait()
   910  			})
   911  		})
   912  		t.Run("PutInFull", func(t *testing.T) {
   913  			p := New(rootCtx,
   914  				WithLimit[*testItem, testItem](1),
   915  				WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   916  				WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   917  				// replace default async closer for sync testing
   918  				WithSyncCloseItem[*testItem, testItem](),
   919  			)
   920  			item := mustGetItem(t, p)
   921  			if err := p.putItem(context.Background(), item); err != nil {
   922  				t.Fatalf("unexpected error on put session into non-full client: %v, wand: %v", err, nil)
   923  			}
   924  
   925  			if err := p.putItem(context.Background(), &testItem{}); !xerrors.Is(err, errPoolIsOverflow) {
   926  				t.Fatalf("unexpected error on put item into full pool: %v, wand: %v", err, errPoolIsOverflow)
   927  			}
   928  		})
   929  		t.Run("PutTwice", func(t *testing.T) {
   930  			p := New(rootCtx,
   931  				WithLimit[*testItem, testItem](2),
   932  				WithCreateItemTimeout[*testItem, testItem](50*time.Millisecond),
   933  				WithCloseItemTimeout[*testItem, testItem](50*time.Millisecond),
   934  				// replace default async closer for sync testing
   935  				WithSyncCloseItem[*testItem, testItem](),
   936  			)
   937  			item := mustGetItem(t, p)
   938  			mustPutItem(t, p, item)
   939  
   940  			require.Panics(t, func() {
   941  				_ = p.putItem(context.Background(), item)
   942  			})
   943  		})
   944  	})
   945  }