github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/internal/table/client_test.go (about)

     1  package table
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"path"
     9  	"runtime"
    10  	"sync"
    11  	"sync/atomic"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/jonboulle/clockwork"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Table"
    18  	"google.golang.org/grpc"
    19  	"google.golang.org/protobuf/proto"
    20  	"google.golang.org/protobuf/types/known/emptypb"
    21  
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/closer"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/table/config"
    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/table"
    30  	"github.com/ydb-platform/ydb-go-sdk/v3/testutil"
    31  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    32  )
    33  
    34  func TestSessionPoolCreateAbnormalResult(t *testing.T) {
    35  	xtest.TestManyTimes(t, func(t testing.TB) {
    36  		limit := 100
    37  		ctx, cancel := xcontext.WithTimeout(
    38  			context.Background(),
    39  			55*time.Second,
    40  		)
    41  		defer cancel()
    42  		p := newClientWithStubBuilder(
    43  			t,
    44  			testutil.NewBalancer(
    45  				testutil.WithInvokeHandlers(
    46  					testutil.InvokeHandlers{
    47  						testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
    48  							return &Ydb_Table.CreateSessionResult{
    49  								SessionId: testutil.SessionID(),
    50  							}, nil
    51  						},
    52  						testutil.TableDeleteSession: okHandler,
    53  					},
    54  				),
    55  			),
    56  			limit,
    57  			config.WithSizeLimit(limit),
    58  		)
    59  		defer func() {
    60  			_ = p.Close(context.Background())
    61  		}()
    62  		r := xrand.New(xrand.WithLock())
    63  		errCh := make(chan error, limit*10)
    64  		fn := func(wg *sync.WaitGroup) {
    65  			defer wg.Done()
    66  			childCtx, childCancel := xcontext.WithTimeout(
    67  				ctx,
    68  				time.Duration(r.Int64(int64(time.Minute))),
    69  			)
    70  			defer childCancel()
    71  			s, err := p.internalPoolCreateSession(childCtx)
    72  			if s == nil && err == nil {
    73  				errCh <- fmt.Errorf("unexpected result: <%v, %w>", s, err)
    74  			}
    75  		}
    76  		wg := &sync.WaitGroup{}
    77  		wg.Add(limit * 10)
    78  		for i := 0; i < limit*10; i++ {
    79  			go fn(wg)
    80  		}
    81  		go func() {
    82  			wg.Wait()
    83  			close(errCh)
    84  		}()
    85  		for e := range errCh {
    86  			t.Fatal(e)
    87  		}
    88  	}, xtest.StopAfter(17*time.Second))
    89  }
    90  
    91  func TestSessionPoolCloseWhenWaiting(t *testing.T) {
    92  	for _, test := range []struct {
    93  		name string
    94  		racy bool
    95  	}{
    96  		{
    97  			name: "normal",
    98  			racy: false,
    99  		},
   100  		{
   101  			name: "racy",
   102  			racy: true,
   103  		},
   104  	} {
   105  		t.Run(test.name, func(t *testing.T) {
   106  			var (
   107  				get  = make(chan struct{})
   108  				wait = make(chan struct{})
   109  				got  = make(chan error)
   110  			)
   111  			p := newClientWithStubBuilder(
   112  				t,
   113  				testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   114  					testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   115  						return &Ydb_Table.CreateSessionResult{
   116  							SessionId: testutil.SessionID(),
   117  						}, nil
   118  					},
   119  				})),
   120  				1,
   121  				config.WithSizeLimit(1),
   122  				config.WithTrace(
   123  					&trace.Table{
   124  						OnPoolWait: func(trace.TablePoolWaitStartInfo) func(trace.TablePoolWaitDoneInfo) {
   125  							wait <- struct{}{}
   126  
   127  							return nil
   128  						},
   129  					},
   130  				),
   131  			)
   132  			defer func() {
   133  				_ = p.Close(context.Background())
   134  			}()
   135  
   136  			mustGetSession(t, p)
   137  
   138  			go func() {
   139  				_, err := p.internalPoolGet(
   140  					context.Background(),
   141  					withTrace(&trace.Table{
   142  						OnPoolGet: func(trace.TablePoolGetStartInfo) func(trace.TablePoolGetDoneInfo) {
   143  							get <- struct{}{}
   144  
   145  							return nil
   146  						},
   147  					}),
   148  				)
   149  				got <- err
   150  			}()
   151  
   152  			regWait := whenWantWaitCh(p)
   153  			<-get     // Await for getter blocked on awaiting session.
   154  			<-regWait // Let the getter register itself in the wait queue.
   155  
   156  			if test.racy {
   157  				// We are testing the case, when session consumer registered
   158  				// himself in the wait queue, but not ready to receive the
   159  				// session when session arrives (that is, stuck between
   160  				// pushing channel in the list and reading from the channel).
   161  				_ = p.Close(context.Background())
   162  				<-wait
   163  			} else {
   164  				// We are testing the normal case, when session consumer registered
   165  				// himself in the wait queue and successfully blocked on
   166  				// reading from signaling channel.
   167  				<-wait
   168  				// Let the waiting goroutine to block on reading from channel.
   169  				_ = p.Close(context.Background())
   170  			}
   171  
   172  			const timeout = time.Second
   173  			select {
   174  			case err := <-got:
   175  				if !xerrors.Is(err, errClosedClient) {
   176  					t.Fatalf(
   177  						"unexpected error: %v; want %v",
   178  						err, errClosedClient,
   179  					)
   180  				}
   181  			case <-p.clock.After(timeout):
   182  				t.Fatalf("no result after %s", timeout)
   183  			}
   184  		})
   185  	}
   186  }
   187  
   188  func TestSessionPoolClose(t *testing.T) {
   189  	counter := 0
   190  	xtest.TestManyTimes(t, func(t testing.TB) {
   191  		counter++
   192  		defer func() {
   193  			if counter%1000 == 0 {
   194  				t.Logf("%d times test passed", counter)
   195  			}
   196  		}()
   197  
   198  		p := newClientWithStubBuilder(
   199  			t,
   200  			testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   201  				testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   202  					return &Ydb_Table.CreateSessionResult{
   203  						SessionId: testutil.SessionID(),
   204  					}, nil
   205  				},
   206  				testutil.TableDeleteSession: func(interface{}) (proto.Message, error) {
   207  					return &Ydb_Table.DeleteSessionResponse{}, nil
   208  				},
   209  			})),
   210  			3,
   211  			config.WithSizeLimit(3),
   212  			config.WithIdleThreshold(time.Hour),
   213  		)
   214  		defer func() {
   215  			_ = p.Close(context.Background())
   216  		}()
   217  
   218  		var (
   219  			s1      = mustGetSession(t, p)
   220  			s2      = mustGetSession(t, p)
   221  			s3      = mustGetSession(t, p)
   222  			closed1 = false
   223  			closed2 = false
   224  			closed3 = false
   225  		)
   226  
   227  		s1.onClose = append(s1.onClose, func(s *session) { closed1 = true })
   228  		s2.onClose = append(s2.onClose, func(s *session) { closed2 = true })
   229  		s3.onClose = append(s3.onClose, func(s *session) { closed3 = true })
   230  
   231  		mustPutSession(t, p, s1)
   232  		mustPutSession(t, p, s2)
   233  		mustClose(t, p)
   234  
   235  		if !closed1 {
   236  			t.Errorf("session1 was not closed")
   237  		}
   238  		if !closed2 {
   239  			t.Errorf("session2 was not closed")
   240  		}
   241  		if closed3 {
   242  			t.Fatalf("unexpected session close")
   243  		}
   244  
   245  		if err := p.Put(context.Background(), s3); !xerrors.Is(err, errClosedClient) {
   246  			t.Errorf(
   247  				"unexpected Put() error: %v; want %v",
   248  				err, errClosedClient,
   249  			)
   250  		}
   251  		if !closed3 {
   252  			t.Fatalf("session was not closed")
   253  		}
   254  	}, xtest.StopAfter(17*time.Second))
   255  }
   256  
   257  func TestRaceWgClosed(t *testing.T) {
   258  	defer func() {
   259  		if e := recover(); e != nil {
   260  			t.Fatal(e)
   261  		}
   262  	}()
   263  
   264  	var (
   265  		limit   = 100
   266  		start   = time.Now()
   267  		counter int
   268  	)
   269  
   270  	xtest.TestManyTimes(t, func(t testing.TB) {
   271  		counter++
   272  		defer func() {
   273  			if counter%1000 == 0 {
   274  				t.Logf("%0.1fs: %d times test passed", time.Since(start).Seconds(), counter)
   275  			}
   276  		}()
   277  		ctx, cancel := xcontext.WithTimeout(context.Background(),
   278  			//nolint:gosec
   279  			time.Duration(rand.Int31n(int32(100*time.Millisecond))),
   280  		)
   281  		defer cancel()
   282  
   283  		wg := sync.WaitGroup{}
   284  		p := newClientWithStubBuilder(t,
   285  			testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   286  				testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   287  					return &Ydb_Table.CreateSessionResult{
   288  						SessionId: testutil.SessionID(),
   289  					}, nil
   290  				},
   291  			})),
   292  			limit,
   293  			config.WithSizeLimit(limit),
   294  		)
   295  		for j := 0; j < limit*10; j++ {
   296  			wg.Add(1)
   297  			go func() {
   298  				defer wg.Done()
   299  				for {
   300  					err := p.Do(ctx,
   301  						func(ctx context.Context, s table.Session) error {
   302  							return nil
   303  						},
   304  					)
   305  					if err != nil && xerrors.Is(err, errClosedClient) {
   306  						return
   307  					}
   308  				}
   309  			}()
   310  		}
   311  		_ = p.Close(context.Background())
   312  		wg.Wait()
   313  	}, xtest.StopAfter(27*time.Second))
   314  }
   315  
   316  func TestSessionPoolDeleteReleaseWait(t *testing.T) {
   317  	for _, test := range []struct {
   318  		name string
   319  		racy bool
   320  	}{
   321  		{
   322  			name: "normal",
   323  			racy: false,
   324  		},
   325  		{
   326  			name: "racy",
   327  			racy: true,
   328  		},
   329  	} {
   330  		t.Run(test.name, func(t *testing.T) {
   331  			var (
   332  				get  = make(chan struct{}, 1)
   333  				wait = make(chan struct{})
   334  				got  = make(chan struct{}, 1)
   335  			)
   336  			p := newClientWithStubBuilder(
   337  				t,
   338  				testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   339  					testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   340  						return &Ydb_Table.CreateSessionResult{
   341  							SessionId: testutil.SessionID(),
   342  						}, nil
   343  					},
   344  				})),
   345  				2,
   346  				config.WithSizeLimit(1),
   347  				config.WithIdleThreshold(time.Hour),
   348  				config.WithTrace(
   349  					&trace.Table{
   350  						OnPoolGet: func(trace.TablePoolGetStartInfo) func(trace.TablePoolGetDoneInfo) {
   351  							get <- struct{}{}
   352  
   353  							return nil
   354  						},
   355  						OnPoolWait: func(trace.TablePoolWaitStartInfo) func(trace.TablePoolWaitDoneInfo) {
   356  							wait <- struct{}{}
   357  
   358  							return nil
   359  						},
   360  					},
   361  				),
   362  			)
   363  			defer func() {
   364  				_ = p.Close(context.Background())
   365  			}()
   366  			s := mustGetSession(t, p)
   367  			go func() {
   368  				defer func() {
   369  					close(got)
   370  				}()
   371  				_, _ = p.Get(context.Background())
   372  			}()
   373  
   374  			regWait := whenWantWaitCh(p)
   375  			<-get     // Await for getter blocked on awaiting session.
   376  			<-regWait // Let the getter register itself in the wait queue.
   377  
   378  			if test.racy {
   379  				// We are testing the case, when session consumer registered
   380  				// himself in the wait queue, but not ready to receive the
   381  				// session when session arrives (that is, it was stucked between
   382  				// pushing channel in the list and reading from the channel).
   383  				_ = s.Close(context.Background())
   384  				<-wait
   385  			} else {
   386  				// We are testing the normal case, when session consumer registered
   387  				// himself in the wait queue and successfully blocked on
   388  				// reading from signaling channel.
   389  				<-wait
   390  				// Let the waiting goroutine to block on reading from channel.
   391  				runtime.Gosched()
   392  				_ = s.Close(context.Background())
   393  			}
   394  
   395  			const timeout = time.Second
   396  			select {
   397  			case <-got:
   398  			case <-p.clock.After(timeout):
   399  				t.Fatalf("no internalPoolGet after %s", timeout)
   400  			}
   401  		})
   402  	}
   403  }
   404  
   405  func TestSessionPoolRacyGet(t *testing.T) {
   406  	type createReq struct {
   407  		release chan struct{}
   408  		session *session
   409  	}
   410  	create := make(chan createReq)
   411  	p, err := newClient(
   412  		context.Background(),
   413  		nil,
   414  		(&StubBuilder{
   415  			Limit: 1,
   416  			OnCreateSession: func(ctx context.Context) (*session, error) {
   417  				req := createReq{
   418  					release: make(chan struct{}),
   419  					session: simpleSession(t),
   420  				}
   421  				create <- req
   422  				<-req.release
   423  
   424  				return req.session, nil
   425  			},
   426  		}).createSession,
   427  		config.New(
   428  			config.WithSizeLimit(1),
   429  			config.WithIdleThreshold(-1),
   430  		),
   431  	)
   432  	require.NoError(t, err)
   433  	var (
   434  		expSession *session
   435  		done       = make(chan struct{}, 2)
   436  	)
   437  	for i := 0; i < 2; i++ {
   438  		go func() {
   439  			defer func() {
   440  				done <- struct{}{}
   441  			}()
   442  			s, e := p.Get(context.Background())
   443  			if e != nil {
   444  				err = e
   445  
   446  				return
   447  			}
   448  			if s != expSession {
   449  				err = fmt.Errorf("unexpected session: %v; want %v", s, expSession)
   450  
   451  				return
   452  			}
   453  			mustPutSession(t, p, s)
   454  		}()
   455  	}
   456  	if err != nil {
   457  		t.Fatal(err)
   458  	}
   459  	// Wait for both requests are created.
   460  	r1 := <-create
   461  	select {
   462  	case <-create:
   463  		t.Fatalf("session 2 on race created while client size 1")
   464  	case <-p.clock.After(time.Millisecond * 5):
   465  		// ok
   466  	}
   467  
   468  	// Release the first create session request.
   469  	// Created session must be stored in the Client.
   470  	expSession = r1.session
   471  	expSession.onClose = append(expSession.onClose, func(s *session) {
   472  		t.Fatalf("unexpected first session close")
   473  	})
   474  	close(r1.release)
   475  
   476  	// Wait for r1's session will be stored in the Client.
   477  	<-done
   478  
   479  	// Ensure that session is in the Client.
   480  	s := mustGetSession(t, p)
   481  	mustPutSession(t, p, s)
   482  }
   483  
   484  func TestSessionPoolPutInFull(t *testing.T) {
   485  	p := newClientWithStubBuilder(
   486  		t,
   487  		testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   488  			testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   489  				return &Ydb_Table.CreateSessionResult{
   490  					SessionId: testutil.SessionID(),
   491  				}, nil
   492  			},
   493  		})),
   494  		1,
   495  		config.WithSizeLimit(1),
   496  		config.WithIdleThreshold(-1),
   497  	)
   498  	s := mustGetSession(t, p)
   499  	if err := p.Put(context.Background(), s); err != nil {
   500  		t.Fatalf("unexpected error on put session into non-full client: %v, wand: %v", err, nil)
   501  	}
   502  
   503  	if err := p.Put(context.Background(), simpleSession(t)); !xerrors.Is(err, errSessionPoolOverflow) {
   504  		t.Fatalf("unexpected error on put session into full client: %v, wand: %v", err, errSessionPoolOverflow)
   505  	}
   506  }
   507  
   508  func TestSessionPoolSizeLimitOverflow(t *testing.T) {
   509  	type sessionAndError struct {
   510  		session *session
   511  		err     error
   512  	}
   513  	for _, test := range []struct {
   514  		name string
   515  		racy bool
   516  	}{
   517  		{
   518  			name: "normal",
   519  			racy: false,
   520  		},
   521  		{
   522  			name: "racy",
   523  			racy: true,
   524  		},
   525  	} {
   526  		t.Run(test.name, func(t *testing.T) {
   527  			var (
   528  				get  = make(chan struct{})
   529  				wait = make(chan struct{})
   530  				got  = make(chan sessionAndError)
   531  			)
   532  			p := newClientWithStubBuilder(
   533  				t,
   534  				testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   535  					testutil.TableCreateSession: func(interface{}) (result proto.Message, _ error) {
   536  						return &Ydb_Table.CreateSessionResult{
   537  							SessionId: testutil.SessionID(),
   538  						}, nil
   539  					},
   540  				})),
   541  				1,
   542  				config.WithSizeLimit(1),
   543  			)
   544  			defer func() {
   545  				_ = p.Close(context.Background())
   546  			}()
   547  			s := mustGetSession(t, p)
   548  			{
   549  				ctx, cancel := xcontext.WithCancel(context.Background())
   550  				cancel()
   551  				if _, err := p.Get(ctx); !xerrors.Is(err, context.Canceled) {
   552  					t.Fatalf(
   553  						"unexpected error: %v; want %v",
   554  						err, context.Canceled,
   555  					)
   556  				}
   557  			}
   558  			go func() {
   559  				session, err := p.internalPoolGet(
   560  					context.Background(),
   561  					withTrace(&trace.Table{
   562  						OnPoolGet: func(trace.TablePoolGetStartInfo) func(trace.TablePoolGetDoneInfo) {
   563  							get <- struct{}{}
   564  
   565  							return nil
   566  						},
   567  						OnPoolWait: func(trace.TablePoolWaitStartInfo) func(trace.TablePoolWaitDoneInfo) {
   568  							wait <- struct{}{}
   569  
   570  							return nil
   571  						},
   572  					}),
   573  				)
   574  				got <- sessionAndError{session, err}
   575  			}()
   576  
   577  			regWait := whenWantWaitCh(p)
   578  			<-get     // Await for getter blocked on awaiting session.
   579  			<-regWait // Let the getter register itself in the wait queue.
   580  
   581  			if test.racy {
   582  				// We are testing the case, when session consumer registered
   583  				// himself in the wait queue, but not ready to receive the
   584  				// session when session arrives (that is, it was stucked between
   585  				// pushing channel in the list and reading from the channel).
   586  				_ = p.Put(context.Background(), s)
   587  				<-wait
   588  			} else {
   589  				// We are testing the normal case, when session consumer registered
   590  				// himself in the wait queue and successfully blocked on
   591  				// reading from signaling channel.
   592  				<-wait
   593  				// Let the waiting goroutine to block on reading from channel.
   594  				_ = p.Put(context.Background(), s)
   595  			}
   596  
   597  			const timeout = time.Second
   598  			select {
   599  			case se := <-got:
   600  				if se.err != nil {
   601  					t.Fatal(se.err)
   602  				}
   603  				if se.session != s {
   604  					t.Fatalf("unexpected session")
   605  				}
   606  			case <-p.clock.After(timeout):
   607  				t.Fatalf("no session after %s", timeout)
   608  			}
   609  		})
   610  	}
   611  }
   612  
   613  func TestSessionPoolGetPut(t *testing.T) {
   614  	var (
   615  		created int
   616  		deleted int
   617  	)
   618  	assertCreated := func(exp int) {
   619  		if act := created; act != exp {
   620  			t.Errorf(
   621  				"unexpected number of created sessions: %v; want %v",
   622  				act, exp,
   623  			)
   624  		}
   625  	}
   626  	assertDeleted := func(exp int) {
   627  		if act := deleted; act != exp {
   628  			t.Errorf(
   629  				"unexpected number of deleted sessions: %v; want %v",
   630  				act, exp,
   631  			)
   632  		}
   633  	}
   634  	p := newClientWithStubBuilder(
   635  		t,
   636  		testutil.NewBalancer(
   637  			testutil.WithInvokeHandlers(
   638  				testutil.InvokeHandlers{
   639  					testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   640  						created++
   641  
   642  						return &Ydb_Table.CreateSessionResult{
   643  							SessionId: testutil.SessionID(),
   644  						}, nil
   645  					},
   646  					testutil.TableDeleteSession: func(interface{}) (proto.Message, error) {
   647  						deleted++
   648  
   649  						return nil, nil
   650  					},
   651  				},
   652  			),
   653  		),
   654  		0,
   655  		config.WithSizeLimit(1),
   656  	)
   657  	defer func() {
   658  		_ = p.Close(context.Background())
   659  	}()
   660  
   661  	s := mustGetSession(t, p)
   662  	assertCreated(1)
   663  
   664  	mustPutSession(t, p, s)
   665  	assertDeleted(0)
   666  
   667  	mustGetSession(t, p)
   668  	assertCreated(1)
   669  
   670  	_ = s.Close(context.Background())
   671  	assertDeleted(1)
   672  
   673  	mustGetSession(t, p)
   674  	assertCreated(2)
   675  }
   676  
   677  func TestSessionPoolCloseIdleSessions(t *testing.T) {
   678  	xtest.TestManyTimes(t, func(t testing.TB) {
   679  		var (
   680  			idleThreshold = 4 * time.Second
   681  			closedCount   atomic.Int64
   682  			fakeClock     = clockwork.NewFakeClock()
   683  		)
   684  		p := newClientWithStubBuilder(
   685  			t,
   686  			testutil.NewBalancer(
   687  				testutil.WithInvokeHandlers(
   688  					testutil.InvokeHandlers{
   689  						testutil.TableDeleteSession: okHandler,
   690  						testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   691  							closedCount.Add(1)
   692  
   693  							return &Ydb_Table.CreateSessionResult{
   694  								SessionId: testutil.SessionID(),
   695  							}, nil
   696  						},
   697  					},
   698  				),
   699  			),
   700  			2,
   701  			config.WithSizeLimit(2),
   702  			config.WithIdleThreshold(idleThreshold),
   703  			config.WithClock(fakeClock),
   704  		)
   705  
   706  		s1 := mustGetSession(t, p)
   707  		s2 := mustGetSession(t, p)
   708  
   709  		// Put both sessions at the absolutely same time.
   710  		// That is, both sessions must be keepalived by a single tick.
   711  		mustPutSession(t, p, s1)
   712  		mustPutSession(t, p, s2)
   713  
   714  		// Emulate first simple tick event. We expect two sessions be keepalived.
   715  		fakeClock.Advance(idleThreshold / 2)
   716  		if !closedCount.CompareAndSwap(2, 0) {
   717  			t.Fatal("unexpected number of keepalives")
   718  		}
   719  
   720  		// Now internalPoolGet first session and "spent" some time working within it.
   721  		x := mustGetSession(t, p)
   722  
   723  		// Move time to idleThreshold / 2
   724  		fakeClock.Advance(idleThreshold / 2)
   725  
   726  		// Now put that session back and emulate keepalive moment.
   727  		mustPutSession(t, p, x)
   728  
   729  		// Move time to idleThreshold / 2
   730  		fakeClock.Advance(idleThreshold / 2)
   731  		// We expect here next tick to be registered after half of a idleThreshold.
   732  		// That is, x was touched half of idleThreshold ago, so we need to wait for
   733  		// the second half until we must touch it.
   734  
   735  		_ = p.Close(context.Background())
   736  	}, xtest.StopAfter(12*time.Second))
   737  }
   738  
   739  func TestSessionPoolDoublePut(t *testing.T) {
   740  	p := newClientWithStubBuilder(
   741  		t,
   742  		testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   743  			testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   744  				return &Ydb_Table.CreateSessionResult{
   745  					SessionId: testutil.SessionID(),
   746  				}, nil
   747  			},
   748  		})),
   749  		1,
   750  		config.WithSizeLimit(2),
   751  		config.WithIdleThreshold(-1),
   752  	)
   753  
   754  	s := mustGetSession(t, p)
   755  	mustPutSession(t, p, s)
   756  
   757  	defer func() {
   758  		if thePanic := recover(); thePanic == nil {
   759  			t.Fatalf("no panic")
   760  		}
   761  	}()
   762  	_ = p.Put(context.Background(), s)
   763  }
   764  
   765  func mustGetSession(t testing.TB, p *Client) *session {
   766  	wg := sync.WaitGroup{}
   767  	defer wg.Wait()
   768  	s, err := p.Get(context.Background())
   769  	if err != nil {
   770  		t.Helper()
   771  		t.Fatalf("%s: %v", caller(), err)
   772  	}
   773  
   774  	return s
   775  }
   776  
   777  func mustPutSession(t testing.TB, p *Client, s *session) {
   778  	wg := sync.WaitGroup{}
   779  	defer wg.Wait()
   780  	if err := p.Put(context.Background(), s); err != nil {
   781  		t.Helper()
   782  		t.Fatalf("%s: %v", caller(), err)
   783  	}
   784  }
   785  
   786  func mustClose(t testing.TB, p closer.Closer) {
   787  	wg := sync.WaitGroup{}
   788  	defer wg.Wait()
   789  	if err := p.Close(context.Background()); err != nil {
   790  		t.Helper()
   791  		t.Fatalf("%s: %v", caller(), err)
   792  	}
   793  }
   794  
   795  func caller() string {
   796  	_, file, line, _ := runtime.Caller(2)
   797  
   798  	return fmt.Sprintf("%s:%d", path.Base(file), line)
   799  }
   800  
   801  var okHandler = func(interface{}) (proto.Message, error) {
   802  	return &emptypb.Empty{}, nil
   803  }
   804  
   805  var simpleCluster = testutil.NewBalancer(
   806  	testutil.WithInvokeHandlers(
   807  		testutil.InvokeHandlers{
   808  			testutil.TableExecuteDataQuery: func(interface{}) (proto.Message, error) {
   809  				return &Ydb_Table.ExecuteQueryResult{
   810  					TxMeta: &Ydb_Table.TransactionMeta{
   811  						Id: "",
   812  					},
   813  				}, nil
   814  			},
   815  			testutil.TableBeginTransaction: func(interface{}) (proto.Message, error) {
   816  				return &Ydb_Table.BeginTransactionResult{
   817  					TxMeta: &Ydb_Table.TransactionMeta{
   818  						Id: "",
   819  					},
   820  				}, nil
   821  			},
   822  			testutil.TableExplainDataQuery: func(interface{}) (proto.Message, error) {
   823  				return &Ydb_Table.ExecuteQueryResult{}, nil
   824  			},
   825  			testutil.TablePrepareDataQuery: func(interface{}) (proto.Message, error) {
   826  				return &Ydb_Table.PrepareQueryResult{}, nil
   827  			},
   828  			testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   829  				return &Ydb_Table.CreateSessionResult{
   830  					SessionId: testutil.SessionID(),
   831  				}, nil
   832  			},
   833  			testutil.TableDeleteSession: func(interface{}) (proto.Message, error) {
   834  				return &Ydb_Table.DeleteSessionResponse{}, nil
   835  			},
   836  			testutil.TableCommitTransaction: func(interface{}) (proto.Message, error) {
   837  				return &Ydb_Table.CommitTransactionResponse{}, nil
   838  			},
   839  			testutil.TableRollbackTransaction: func(interface{}) (proto.Message, error) {
   840  				return &Ydb_Table.RollbackTransactionResponse{}, nil
   841  			},
   842  			testutil.TableKeepAlive: func(interface{}) (proto.Message, error) {
   843  				return &Ydb_Table.KeepAliveResult{}, nil
   844  			},
   845  		},
   846  	),
   847  )
   848  
   849  func simpleSession(t *testing.T) *session {
   850  	s, err := newSession(context.Background(), simpleCluster, config.New())
   851  	if err != nil {
   852  		t.Fatalf("newSession unexpected error: %v", err)
   853  	}
   854  
   855  	return s
   856  }
   857  
   858  type StubBuilder struct {
   859  	OnCreateSession func(ctx context.Context) (*session, error)
   860  
   861  	cc    grpc.ClientConnInterface
   862  	Limit int
   863  	T     testing.TB
   864  
   865  	mu     xsync.Mutex
   866  	actual int
   867  }
   868  
   869  func newClientWithStubBuilder(
   870  	t testing.TB,
   871  	balancer balancer,
   872  	stubLimit int,
   873  	options ...config.Option,
   874  ) *Client {
   875  	c, err := newClient(
   876  		context.Background(),
   877  		balancer,
   878  		(&StubBuilder{
   879  			T:     t,
   880  			Limit: stubLimit,
   881  			cc:    balancer,
   882  		}).createSession,
   883  		config.New(options...),
   884  	)
   885  	require.NoError(t, err)
   886  
   887  	return c
   888  }
   889  
   890  func (s *StubBuilder) createSession(ctx context.Context) (session *session, err error) {
   891  	defer s.mu.WithLock(func() {
   892  		if session != nil {
   893  			s.actual++
   894  		}
   895  	})
   896  
   897  	s.mu.WithLock(func() {
   898  		if s.Limit > 0 && s.actual == s.Limit {
   899  			err = fmt.Errorf("stub session: limit overflow")
   900  		}
   901  	})
   902  	if err != nil {
   903  		return nil, err
   904  	}
   905  
   906  	if f := s.OnCreateSession; f != nil {
   907  		return f(ctx)
   908  	}
   909  
   910  	return newSession(ctx, s.cc, config.New())
   911  }
   912  
   913  func (c *Client) debug() {
   914  	fmt.Print("head ")
   915  	for el := c.idle.Front(); el != nil; el = el.Next() {
   916  		s := el.Value.(*session)
   917  		x := c.index[s]
   918  		fmt.Printf("<-> %s(%d) ", s.ID(), x.touched.Unix())
   919  	}
   920  	fmt.Print("<-> tail\n")
   921  }
   922  
   923  func whenWantWaitCh(p *Client) <-chan struct{} {
   924  	var (
   925  		prev = p.testHookGetWaitCh
   926  		ch   = make(chan struct{})
   927  	)
   928  	p.testHookGetWaitCh = func() {
   929  		p.testHookGetWaitCh = prev
   930  		close(ch)
   931  	}
   932  
   933  	return ch
   934  }
   935  
   936  func TestDeadlockOnUpdateNodes(t *testing.T) {
   937  	xtest.TestManyTimes(t, func(t testing.TB) {
   938  		ctx, cancel := xcontext.WithTimeout(context.Background(), 1*time.Second)
   939  		defer cancel()
   940  		var (
   941  			nodes         = make([]uint32, 0, 3)
   942  			nodeIDCounter = uint32(0)
   943  		)
   944  		balancer := testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   945  			testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   946  				sessionID := testutil.SessionID(testutil.WithNodeID(nodeIDCounter))
   947  				nodeIDCounter++
   948  				nodeID, err := nodeID(sessionID)
   949  				if err != nil {
   950  					return nil, err
   951  				}
   952  				nodes = append(nodes, nodeID)
   953  
   954  				return &Ydb_Table.CreateSessionResult{
   955  					SessionId: sessionID,
   956  				}, nil
   957  			},
   958  		}))
   959  		c := newClientWithStubBuilder(t, balancer, 3)
   960  		defer func() {
   961  			_ = c.Close(ctx)
   962  		}()
   963  		s1, err := c.Get(ctx)
   964  		require.NoError(t, err)
   965  		s2, err := c.Get(ctx)
   966  		require.NoError(t, err)
   967  		s3, err := c.Get(ctx)
   968  		require.NoError(t, err)
   969  		require.Len(t, nodes, 3)
   970  		err = c.Put(ctx, s1)
   971  		require.NoError(t, err)
   972  		err = c.Put(ctx, s2)
   973  		require.NoError(t, err)
   974  		err = c.Put(ctx, s3)
   975  		require.NoError(t, err)
   976  	}, xtest.StopAfter(12*time.Second))
   977  }
   978  
   979  func TestDeadlockOnInternalPoolGCTick(t *testing.T) {
   980  	xtest.TestManyTimes(t, func(t testing.TB) {
   981  		ctx, cancel := xcontext.WithTimeout(context.Background(), 1*time.Second)
   982  		defer cancel()
   983  		var (
   984  			nodes         = make([]uint32, 0, 3)
   985  			nodeIDCounter = uint32(0)
   986  		)
   987  		balancer := testutil.NewBalancer(testutil.WithInvokeHandlers(testutil.InvokeHandlers{
   988  			testutil.TableCreateSession: func(interface{}) (proto.Message, error) {
   989  				sessionID := testutil.SessionID(testutil.WithNodeID(nodeIDCounter))
   990  				nodeIDCounter++
   991  				nodeID, err := nodeID(sessionID)
   992  				if err != nil {
   993  					return nil, err
   994  				}
   995  				nodes = append(nodes, nodeID)
   996  
   997  				return &Ydb_Table.CreateSessionResult{
   998  					SessionId: sessionID,
   999  				}, nil
  1000  			},
  1001  		}))
  1002  		c := newClientWithStubBuilder(t, balancer, 3)
  1003  		defer func() {
  1004  			_ = c.Close(ctx)
  1005  		}()
  1006  		s1, err := c.Get(ctx)
  1007  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1008  			return
  1009  		}
  1010  		require.NoError(t, err)
  1011  		s2, err := c.Get(ctx)
  1012  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1013  			return
  1014  		}
  1015  		require.NoError(t, err)
  1016  		s3, err := c.Get(ctx)
  1017  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1018  			return
  1019  		}
  1020  		require.NoError(t, err)
  1021  		require.Len(t, nodes, 3)
  1022  		err = c.Put(ctx, s1)
  1023  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1024  			return
  1025  		}
  1026  		require.NoError(t, err)
  1027  		err = c.Put(ctx, s2)
  1028  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1029  			return
  1030  		}
  1031  		require.NoError(t, err)
  1032  		err = c.Put(ctx, s3)
  1033  		if err != nil && errors.Is(err, context.DeadlineExceeded) {
  1034  			return
  1035  		}
  1036  		require.NoError(t, err)
  1037  		c.internalPoolGCTick(ctx, 0)
  1038  	}, xtest.StopAfter(12*time.Second))
  1039  }