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

     1  package table
     2  
     3  import (
     4  	"container/list"
     5  	"context"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/jonboulle/clockwork"
    11  	"google.golang.org/grpc"
    12  
    13  	metaHeaders "github.com/ydb-platform/ydb-go-sdk/v3/internal/meta"
    14  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
    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/xsync"
    19  	"github.com/ydb-platform/ydb-go-sdk/v3/meta"
    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/trace"
    23  )
    24  
    25  // sessionBuilder is the interface that holds logic of creating sessions.
    26  type sessionBuilder func(ctx context.Context) (*session, error)
    27  
    28  type nodeChecker interface {
    29  	HasNode(id uint32) bool
    30  }
    31  
    32  type balancer interface {
    33  	grpc.ClientConnInterface
    34  	nodeChecker
    35  }
    36  
    37  func New(ctx context.Context, balancer balancer, config *config.Config) (*Client, error) {
    38  	return newClient(ctx, balancer, func(ctx context.Context) (s *session, err error) {
    39  		return newSession(ctx, balancer, config)
    40  	}, config)
    41  }
    42  
    43  func newClient(
    44  	ctx context.Context,
    45  	balancer balancer,
    46  	builder sessionBuilder,
    47  	config *config.Config,
    48  ) (c *Client, finalErr error) {
    49  	onDone := trace.TableOnInit(config.Trace(), &ctx, stack.FunctionID(""))
    50  	defer func() {
    51  		onDone(config.SizeLimit(), finalErr)
    52  	}()
    53  	c = &Client{
    54  		clock:       config.Clock(),
    55  		config:      config,
    56  		cc:          balancer,
    57  		nodeChecker: balancer,
    58  		build:       builder,
    59  		index:       make(map[*session]sessionInfo),
    60  		idle:        list.New(),
    61  		waitQ:       list.New(),
    62  		limit:       config.SizeLimit(),
    63  		waitChPool: sync.Pool{
    64  			New: func() interface{} {
    65  				ch := make(chan *session)
    66  
    67  				return &ch
    68  			},
    69  		},
    70  		done: make(chan struct{}),
    71  	}
    72  	if idleThreshold := config.IdleThreshold(); idleThreshold > 0 {
    73  		c.wg.Add(1)
    74  		go c.internalPoolGC(ctx, idleThreshold)
    75  	}
    76  
    77  	return c, nil
    78  }
    79  
    80  // Client is a set of session instances that may be reused.
    81  // A Client is safe for use by multiple goroutines simultaneously.
    82  type Client struct {
    83  	// read-only fields
    84  	config      *config.Config
    85  	build       sessionBuilder
    86  	cc          grpc.ClientConnInterface
    87  	nodeChecker nodeChecker
    88  	clock       clockwork.Clock
    89  
    90  	// read-write fields
    91  	mu                xsync.Mutex
    92  	index             map[*session]sessionInfo
    93  	createInProgress  int        // KIKIMR-9163: in-create-process counter
    94  	limit             int        // Upper bound for Client size.
    95  	idle              *list.List // list<*session>
    96  	waitQ             *list.List // list<*chan *session>
    97  	waitChPool        sync.Pool
    98  	testHookGetWaitCh func() // nil except some tests.
    99  	wg                sync.WaitGroup
   100  	done              chan struct{}
   101  }
   102  
   103  type createSessionOptions struct {
   104  	onCreate []func(s *session)
   105  	onClose  []func(s *session)
   106  }
   107  
   108  type createSessionOption func(o *createSessionOptions)
   109  
   110  func withCreateSessionOnCreate(onCreate func(s *session)) createSessionOption {
   111  	return func(o *createSessionOptions) {
   112  		o.onCreate = append(o.onCreate, onCreate)
   113  	}
   114  }
   115  
   116  func withCreateSessionOnClose(onClose func(s *session)) createSessionOption {
   117  	return func(o *createSessionOptions) {
   118  		o.onClose = append(o.onClose, onClose)
   119  	}
   120  }
   121  
   122  func (c *Client) createSession(ctx context.Context, opts ...createSessionOption) (s *session, err error) {
   123  	options := createSessionOptions{}
   124  	for _, o := range opts {
   125  		if o != nil {
   126  			o(&options)
   127  		}
   128  	}
   129  
   130  	defer func() {
   131  		if s == nil {
   132  			return
   133  		}
   134  		for _, onCreate := range options.onCreate {
   135  			onCreate(s)
   136  		}
   137  		s.onClose = append(s.onClose, options.onClose...)
   138  	}()
   139  
   140  	type result struct {
   141  		s   *session
   142  		err error
   143  	}
   144  
   145  	ch := make(chan result)
   146  
   147  	select {
   148  	case <-c.done:
   149  		return nil, xerrors.WithStackTrace(errClosedClient)
   150  
   151  	case <-ctx.Done():
   152  		return nil, xerrors.WithStackTrace(ctx.Err())
   153  
   154  	default:
   155  		c.mu.WithLock(func() {
   156  			if c.isClosed() {
   157  				return
   158  			}
   159  			c.wg.Add(1)
   160  			go func() {
   161  				defer c.wg.Done()
   162  
   163  				var (
   164  					s   *session
   165  					err error
   166  				)
   167  
   168  				createSessionCtx := xcontext.WithoutDeadline(ctx)
   169  
   170  				if timeout := c.config.CreateSessionTimeout(); timeout > 0 {
   171  					var cancel context.CancelFunc
   172  					createSessionCtx, cancel = xcontext.WithTimeout(createSessionCtx, timeout)
   173  					defer cancel()
   174  				}
   175  
   176  				closeSession := func(s *session) {
   177  					if s == nil {
   178  						return
   179  					}
   180  
   181  					closeSessionCtx := xcontext.WithoutDeadline(ctx)
   182  
   183  					if timeout := c.config.DeleteTimeout(); timeout > 0 {
   184  						var cancel context.CancelFunc
   185  						createSessionCtx, cancel = xcontext.WithTimeout(closeSessionCtx, timeout)
   186  						defer cancel()
   187  					}
   188  
   189  					_ = s.Close(closeSessionCtx)
   190  				}
   191  
   192  				s, err = c.build(createSessionCtx)
   193  
   194  				select {
   195  				case ch <- result{
   196  					s:   s,
   197  					err: err,
   198  				}: // nop
   199  
   200  				case <-c.done:
   201  					closeSession(s)
   202  
   203  				case <-ctx.Done():
   204  					closeSession(s)
   205  				}
   206  			}()
   207  		})
   208  	}
   209  
   210  	select {
   211  	case <-c.done:
   212  		return nil, xerrors.WithStackTrace(errClosedClient)
   213  
   214  	case <-ctx.Done():
   215  		return nil, xerrors.WithStackTrace(ctx.Err())
   216  
   217  	case r := <-ch:
   218  		if r.err != nil {
   219  			return nil, xerrors.WithStackTrace(r.err)
   220  		}
   221  
   222  		return r.s, nil
   223  	}
   224  }
   225  
   226  func (c *Client) CreateSession(ctx context.Context, opts ...table.Option) (_ table.ClosableSession, err error) {
   227  	if c == nil {
   228  		return nil, xerrors.WithStackTrace(errNilClient)
   229  	}
   230  	if c.isClosed() {
   231  		return nil, xerrors.WithStackTrace(errClosedClient)
   232  	}
   233  	var s *session
   234  	createSession := func(ctx context.Context) (*session, error) {
   235  		s, err = c.createSession(ctx)
   236  		if err != nil {
   237  			return nil, xerrors.WithStackTrace(err)
   238  		}
   239  
   240  		return s, nil
   241  	}
   242  	if !c.config.AutoRetry() {
   243  		s, err = createSession(ctx)
   244  		if err != nil {
   245  			return nil, xerrors.WithStackTrace(err)
   246  		}
   247  
   248  		return s, nil
   249  	}
   250  	err = retry.Retry(ctx,
   251  		func(ctx context.Context) (err error) {
   252  			s, err = createSession(ctx)
   253  			if err != nil {
   254  				return xerrors.WithStackTrace(err)
   255  			}
   256  
   257  			return nil
   258  		},
   259  		append(
   260  			[]retry.Option{
   261  				retry.WithIdempotent(true),
   262  				retry.WithTrace(&trace.Retry{
   263  					OnRetry: func(info trace.RetryLoopStartInfo) func(trace.RetryLoopIntermediateInfo) func(trace.RetryLoopDoneInfo) {
   264  						onIntermediate := trace.TableOnCreateSession(c.config.Trace(), info.Context, stack.FunctionID(""))
   265  
   266  						return func(info trace.RetryLoopIntermediateInfo) func(trace.RetryLoopDoneInfo) {
   267  							onDone := onIntermediate(info.Error)
   268  
   269  							return func(info trace.RetryLoopDoneInfo) {
   270  								onDone(s, info.Attempts, info.Error)
   271  							}
   272  						}
   273  					},
   274  				}),
   275  			}, c.retryOptions(opts...).RetryOptions...,
   276  		)...,
   277  	)
   278  
   279  	return s, xerrors.WithStackTrace(err)
   280  }
   281  
   282  func (c *Client) isClosed() bool {
   283  	select {
   284  	case <-c.done:
   285  		return true
   286  	default:
   287  		return false
   288  	}
   289  }
   290  
   291  // c.mu must NOT be held.
   292  func (c *Client) internalPoolCreateSession(ctx context.Context) (s *session, err error) {
   293  	if c.isClosed() {
   294  		return nil, errClosedClient
   295  	}
   296  	// pre-check the Client size
   297  	var enoughSpace bool
   298  	c.mu.WithLock(func() {
   299  		enoughSpace = c.createInProgress+len(c.index) < c.limit
   300  		if enoughSpace {
   301  			c.createInProgress++
   302  		}
   303  	})
   304  
   305  	if !enoughSpace {
   306  		return nil, xerrors.WithStackTrace(errSessionPoolOverflow)
   307  	}
   308  
   309  	defer func() {
   310  		c.mu.WithLock(func() {
   311  			c.createInProgress--
   312  		})
   313  	}()
   314  
   315  	s, err = c.createSession(
   316  		meta.WithAllowFeatures(ctx,
   317  			metaHeaders.HintSessionBalancer,
   318  		),
   319  		withCreateSessionOnCreate(func(s *session) {
   320  			c.mu.WithLock(func() {
   321  				c.index[s] = sessionInfo{
   322  					touched: c.clock.Now(),
   323  				}
   324  				trace.TableOnPoolSessionAdd(c.config.Trace(), s)
   325  				trace.TableOnPoolStateChange(c.config.Trace(), len(c.index), "append")
   326  			})
   327  		}), withCreateSessionOnClose(func(s *session) {
   328  			c.mu.WithLock(func() {
   329  				info, has := c.index[s]
   330  				if !has {
   331  					panic("session not found in pool")
   332  				}
   333  
   334  				delete(c.index, s)
   335  
   336  				trace.TableOnPoolSessionRemove(c.config.Trace(), s)
   337  				trace.TableOnPoolStateChange(c.config.Trace(), len(c.index), "remove")
   338  
   339  				if !c.isClosed() {
   340  					c.internalPoolNotify(nil)
   341  				}
   342  
   343  				if info.idle != nil {
   344  					c.idle.Remove(info.idle)
   345  				}
   346  			})
   347  		}))
   348  	if err != nil {
   349  		return nil, xerrors.WithStackTrace(err)
   350  	}
   351  
   352  	return s, nil
   353  }
   354  
   355  type getOptions struct {
   356  	t *trace.Table
   357  }
   358  
   359  type getOption func(o *getOptions)
   360  
   361  func withTrace(t *trace.Table) getOption {
   362  	return func(o *getOptions) {
   363  		o.t = o.t.Compose(t)
   364  	}
   365  }
   366  
   367  func (c *Client) internalPoolGet(ctx context.Context, opts ...getOption) (s *session, err error) {
   368  	if c.isClosed() {
   369  		return nil, xerrors.WithStackTrace(errClosedClient)
   370  	}
   371  
   372  	var (
   373  		start = time.Now()
   374  		i     = 0
   375  		o     = getOptions{t: c.config.Trace()}
   376  	)
   377  	for _, opt := range opts {
   378  		if opt != nil {
   379  			opt(&o)
   380  		}
   381  	}
   382  
   383  	onDone := trace.TableOnPoolGet(o.t, &ctx, stack.FunctionID(""))
   384  	defer func() {
   385  		onDone(s, i, err)
   386  	}()
   387  
   388  	const maxAttempts = 100
   389  	for s == nil && err == nil && i < maxAttempts && !c.isClosed() {
   390  		i++
   391  		// First, we try to internalPoolGet session from idle
   392  		c.mu.WithLock(func() {
   393  			s = c.internalPoolRemoveFirstIdle()
   394  		})
   395  
   396  		if s != nil {
   397  			if c.nodeChecker != nil && !c.nodeChecker.HasNode(s.NodeID()) {
   398  				_ = s.Close(ctx)
   399  				s = nil
   400  
   401  				continue
   402  			}
   403  
   404  			return s, nil
   405  		}
   406  
   407  		// Second, we try to create new session
   408  		s, err = c.internalPoolCreateSession(ctx)
   409  		if s == nil && err == nil {
   410  			if err = ctx.Err(); err != nil {
   411  				return nil, xerrors.WithStackTrace(err)
   412  			}
   413  			panic("both of session and err are nil")
   414  		}
   415  		// got session or err is not recoverable
   416  		if s != nil || !isCreateSessionErrorRetriable(err) {
   417  			return s, xerrors.WithStackTrace(err)
   418  		}
   419  
   420  		// Third, we try to wait for a touched session - Client is full.
   421  		//
   422  		// This should be done only if number of currently waiting goroutines
   423  		// are less than maximum amount of touched session. That is, we want to
   424  		// be fair here and not to lock more goroutines than we could ship
   425  		// session to.
   426  		s, err = c.internalPoolWaitFromCh(ctx, o.t)
   427  		if err != nil {
   428  			err = xerrors.WithStackTrace(err)
   429  		}
   430  	}
   431  	if s == nil && err == nil {
   432  		if c.isClosed() {
   433  			err = xerrors.WithStackTrace(errClosedClient)
   434  		} else {
   435  			err = xerrors.WithStackTrace(errNoProgress)
   436  		}
   437  	}
   438  	if err != nil {
   439  		var (
   440  			index            int
   441  			idle             int
   442  			createInProgress int
   443  		)
   444  		c.mu.WithLock(func() {
   445  			index = len(c.index)
   446  			idle = c.idle.Len()
   447  			createInProgress = c.createInProgress
   448  		})
   449  
   450  		return s, xerrors.WithStackTrace(
   451  			fmt.Errorf("failed to get session from pool ("+
   452  				"attempts: %d, latency: %v, pool have %d sessions (%d busy, %d idle, %d create_in_progress): %w",
   453  				i, time.Since(start), index, index-idle, idle, createInProgress, err,
   454  			),
   455  		)
   456  	}
   457  
   458  	return s, nil
   459  }
   460  
   461  // Get returns first idle session from the Client and removes it from
   462  // there. If no items stored in Client it creates new one returns it.
   463  func (c *Client) Get(ctx context.Context) (s *session, err error) {
   464  	return c.internalPoolGet(ctx)
   465  }
   466  
   467  func (c *Client) internalPoolWaitFromCh(ctx context.Context, t *trace.Table) (s *session, err error) {
   468  	var (
   469  		ch *chan *session
   470  		el *list.Element // Element in the wait queue.
   471  		ok bool
   472  	)
   473  
   474  	c.mu.WithLock(func() {
   475  		ch = c.internalPoolGetWaitCh()
   476  		el = c.waitQ.PushBack(ch)
   477  	})
   478  
   479  	waitDone := trace.TableOnPoolWait(t, &ctx, stack.FunctionID(""))
   480  
   481  	defer func() {
   482  		waitDone(s, err)
   483  	}()
   484  
   485  	var createSessionTimeoutCh <-chan time.Time
   486  	if timeout := c.config.CreateSessionTimeout(); timeout > 0 {
   487  		createSessionTimeoutCh = c.clock.After(timeout)
   488  	}
   489  
   490  	select {
   491  	case <-c.done:
   492  		c.mu.WithLock(func() {
   493  			c.waitQ.Remove(el)
   494  		})
   495  
   496  		return nil, xerrors.WithStackTrace(errClosedClient)
   497  
   498  	case s, ok = <-*ch:
   499  		// Note that race may occur and some goroutine may try to write
   500  		// session into channel after it was enqueued but before it being
   501  		// read here. In that case we will receive nil here and will retry.
   502  		//
   503  		// The same way will work when some session become deleted - the
   504  		// nil value will be sent into the channel.
   505  		if ok {
   506  			// Put only filled and not closed channel back to the Client.
   507  			// That is, we need to avoid races on filling reused channel
   508  			// for the next waiter – session could be lost for a long time.
   509  			c.internalPoolPutWaitCh(ch)
   510  		}
   511  
   512  		return s, nil
   513  
   514  	case <-createSessionTimeoutCh:
   515  		c.mu.WithLock(func() {
   516  			c.waitQ.Remove(el)
   517  		})
   518  
   519  		return nil, nil //nolint:nilnil
   520  
   521  	case <-ctx.Done():
   522  		c.mu.WithLock(func() {
   523  			c.waitQ.Remove(el)
   524  		})
   525  
   526  		return nil, xerrors.WithStackTrace(ctx.Err())
   527  	}
   528  }
   529  
   530  // Put returns session to the Client for further reuse.
   531  // If Client is already closed Put() calls s.Close(ctx) and returns
   532  // errClosedClient.
   533  // If Client is overflow calls s.Close(ctx) and returns
   534  // errSessionPoolOverflow.
   535  //
   536  // Note that Put() must be called only once after being created or received by
   537  // Get() or Take() calls. In other way it will produce unexpected behavior or
   538  // panic.
   539  func (c *Client) Put(ctx context.Context, s *session) (err error) {
   540  	onDone := trace.TableOnPoolPut(c.config.Trace(), &ctx,
   541  		stack.FunctionID(""),
   542  		s,
   543  	)
   544  	defer func() {
   545  		onDone(err)
   546  	}()
   547  
   548  	defer func() {
   549  		if err != nil {
   550  			c.internalPoolSyncCloseSession(ctx, s)
   551  		}
   552  	}()
   553  
   554  	switch {
   555  	case c.isClosed():
   556  		return xerrors.WithStackTrace(errClosedClient)
   557  
   558  	case s.isClosing():
   559  		return xerrors.WithStackTrace(errSessionUnderShutdown)
   560  
   561  	case s.isClosed():
   562  		return xerrors.WithStackTrace(errSessionClosed)
   563  
   564  	case c.nodeChecker != nil && !c.nodeChecker.HasNode(s.NodeID()):
   565  		return xerrors.WithStackTrace(errNodeIsNotObservable)
   566  
   567  	default:
   568  		c.mu.Lock()
   569  		defer c.mu.Unlock()
   570  
   571  		if c.idle.Len() >= c.limit {
   572  			return xerrors.WithStackTrace(errSessionPoolOverflow)
   573  		}
   574  
   575  		if !c.internalPoolNotify(s) {
   576  			c.internalPoolPushIdle(s, c.clock.Now())
   577  		}
   578  
   579  		return nil
   580  	}
   581  }
   582  
   583  // Close deletes all stored sessions inside Client.
   584  // It also stops all underlying timers and goroutines.
   585  // It returns first error occurred during stale sessions' deletion.
   586  // Note that even on error it calls Close() on each session.
   587  func (c *Client) Close(ctx context.Context) (err error) {
   588  	if c == nil {
   589  		return xerrors.WithStackTrace(errNilClient)
   590  	}
   591  
   592  	c.mu.WithLock(func() {
   593  		select {
   594  		case <-c.done:
   595  			return
   596  
   597  		default:
   598  			close(c.done)
   599  
   600  			onDone := trace.TableOnClose(c.config.Trace(), &ctx, stack.FunctionID(""))
   601  			defer func() {
   602  				onDone(err)
   603  			}()
   604  
   605  			c.limit = 0
   606  
   607  			for el := c.waitQ.Front(); el != nil; el = el.Next() {
   608  				ch := el.Value.(*chan *session)
   609  				close(*ch)
   610  			}
   611  
   612  			for e := c.idle.Front(); e != nil; e = e.Next() {
   613  				s := e.Value.(*session)
   614  				s.SetStatus(table.SessionClosing)
   615  				c.wg.Add(1)
   616  				go func() {
   617  					defer c.wg.Done()
   618  					c.internalPoolSyncCloseSession(ctx, s)
   619  				}()
   620  			}
   621  		}
   622  	})
   623  
   624  	c.wg.Wait()
   625  
   626  	return nil
   627  }
   628  
   629  // Do provide the best effort for execute operation
   630  // Do implements internal busy loop until one of the following conditions is met:
   631  // - deadline was canceled or deadlined
   632  // - retry operation returned nil as error
   633  // Warning: if deadline without deadline or cancellation func Retry will be worked infinite
   634  func (c *Client) Do(ctx context.Context, op table.Operation, opts ...table.Option) (finalErr error) {
   635  	if c == nil {
   636  		return xerrors.WithStackTrace(errNilClient)
   637  	}
   638  
   639  	if c.isClosed() {
   640  		return xerrors.WithStackTrace(errClosedClient)
   641  	}
   642  
   643  	config := c.retryOptions(opts...)
   644  
   645  	attempts, onIntermediate := 0, trace.TableOnDo(config.Trace, &ctx,
   646  		stack.FunctionID(""),
   647  		config.Label, config.Label, config.Idempotent, xcontext.IsNestedCall(ctx),
   648  	)
   649  	defer func() {
   650  		onIntermediate(finalErr)(attempts, finalErr)
   651  	}()
   652  
   653  	err := do(ctx, c, c.config, op, func(err error) {
   654  		attempts++
   655  		onIntermediate(err)
   656  	}, config.RetryOptions...)
   657  	if err != nil {
   658  		return xerrors.WithStackTrace(err)
   659  	}
   660  
   661  	return nil
   662  }
   663  
   664  func (c *Client) DoTx(ctx context.Context, op table.TxOperation, opts ...table.Option) (finalErr error) {
   665  	if c == nil {
   666  		return xerrors.WithStackTrace(errNilClient)
   667  	}
   668  
   669  	if c.isClosed() {
   670  		return xerrors.WithStackTrace(errClosedClient)
   671  	}
   672  
   673  	config := c.retryOptions(opts...)
   674  
   675  	attempts, onIntermediate := 0, trace.TableOnDoTx(config.Trace, &ctx,
   676  		stack.FunctionID(""),
   677  		config.Label, config.Label, config.Idempotent, xcontext.IsNestedCall(ctx),
   678  	)
   679  	defer func() {
   680  		onIntermediate(finalErr)(attempts, finalErr)
   681  	}()
   682  
   683  	return retryBackoff(ctx, c,
   684  		func(ctx context.Context, s table.Session) (err error) {
   685  			attempts++
   686  
   687  			defer func() {
   688  				onIntermediate(err)
   689  			}()
   690  
   691  			tx, err := s.BeginTransaction(ctx, config.TxSettings)
   692  			if err != nil {
   693  				return xerrors.WithStackTrace(err)
   694  			}
   695  
   696  			defer func() {
   697  				if err != nil {
   698  					errRollback := tx.Rollback(ctx)
   699  					if errRollback != nil {
   700  						err = xerrors.NewWithIssues("",
   701  							xerrors.WithStackTrace(err),
   702  							xerrors.WithStackTrace(errRollback),
   703  						)
   704  					} else {
   705  						err = xerrors.WithStackTrace(err)
   706  					}
   707  				}
   708  			}()
   709  
   710  			err = func() error {
   711  				if panicCallback := c.config.PanicCallback(); panicCallback != nil {
   712  					defer func() {
   713  						if e := recover(); e != nil {
   714  							panicCallback(e)
   715  						}
   716  					}()
   717  				}
   718  
   719  				return op(xcontext.MarkRetryCall(ctx), tx)
   720  			}()
   721  
   722  			if err != nil {
   723  				return xerrors.WithStackTrace(err)
   724  			}
   725  
   726  			_, err = tx.CommitTx(ctx, config.TxCommitOptions...)
   727  			if err != nil {
   728  				return xerrors.WithStackTrace(err)
   729  			}
   730  
   731  			return nil
   732  		},
   733  		config.RetryOptions...,
   734  	)
   735  }
   736  
   737  func (c *Client) internalPoolGCTick(ctx context.Context, idleThreshold time.Duration) {
   738  	c.mu.WithLock(func() {
   739  		if c.isClosed() {
   740  			return
   741  		}
   742  		for e := c.idle.Front(); e != nil; e = e.Next() {
   743  			s := e.Value.(*session)
   744  			info, has := c.index[s]
   745  			if !has {
   746  				panic("session not found in pool")
   747  			}
   748  			if info.idle == nil {
   749  				panic("inconsistent session info")
   750  			}
   751  			if since := c.clock.Since(info.touched); since > idleThreshold {
   752  				s.SetStatus(table.SessionClosing)
   753  				c.wg.Add(1)
   754  				go func() {
   755  					defer c.wg.Done()
   756  					c.internalPoolSyncCloseSession(ctx, s)
   757  				}()
   758  			}
   759  		}
   760  	})
   761  }
   762  
   763  func (c *Client) internalPoolGC(ctx context.Context, idleThreshold time.Duration) {
   764  	defer c.wg.Done()
   765  
   766  	timer := c.clock.NewTimer(idleThreshold)
   767  	defer timer.Stop()
   768  
   769  	for {
   770  		select {
   771  		case <-c.done:
   772  			return
   773  
   774  		case <-ctx.Done():
   775  			return
   776  
   777  		case <-timer.Chan():
   778  			c.internalPoolGCTick(ctx, idleThreshold)
   779  			timer.Reset(idleThreshold / 2)
   780  		}
   781  	}
   782  }
   783  
   784  // internalPoolGetWaitCh returns pointer to a channel of sessions.
   785  //
   786  // Note that returning a pointer reduces allocations on sync.Pool usage –
   787  // sync.Client.Get() returns empty interface, which leads to allocation for
   788  // non-pointer values.
   789  func (c *Client) internalPoolGetWaitCh() *chan *session { //nolint:gocritic
   790  	if c.testHookGetWaitCh != nil {
   791  		c.testHookGetWaitCh()
   792  	}
   793  	ch := c.waitChPool.Get()
   794  	s, ok := ch.(*chan *session)
   795  	if !ok {
   796  		panic(fmt.Sprintf("%T is not a chan of sessions", ch))
   797  	}
   798  
   799  	return s
   800  }
   801  
   802  // internalPoolPutWaitCh receives pointer to a channel and makes it available for further
   803  // use.
   804  // Note that ch MUST NOT be owned by any goroutine at the call moment and ch
   805  // MUST NOT contain any value.
   806  func (c *Client) internalPoolPutWaitCh(ch *chan *session) { //nolint:gocritic
   807  	c.waitChPool.Put(ch)
   808  }
   809  
   810  // c.mu must be held.
   811  func (c *Client) internalPoolPeekFirstIdle() (s *session, touched time.Time) {
   812  	el := c.idle.Front()
   813  	if el == nil {
   814  		return
   815  	}
   816  	s = el.Value.(*session)
   817  	info, has := c.index[s]
   818  	if !has || el != info.idle {
   819  		panic("inconsistent session client index")
   820  	}
   821  
   822  	return s, info.touched
   823  }
   824  
   825  // removes first session from idle and resets the keepAliveCount
   826  // to prevent session from dying in the internalPoolGC after it was returned
   827  // to be used only in outgoing functions that make session busy.
   828  // c.mu must be held.
   829  func (c *Client) internalPoolRemoveFirstIdle() *session {
   830  	s, _ := c.internalPoolPeekFirstIdle()
   831  	if s != nil {
   832  		info := c.internalPoolRemoveIdle(s)
   833  		c.index[s] = info
   834  	}
   835  
   836  	return s
   837  }
   838  
   839  // c.mu must be held.
   840  func (c *Client) internalPoolNotify(s *session) (notified bool) {
   841  	for el := c.waitQ.Front(); el != nil; el = c.waitQ.Front() {
   842  		// Some goroutine is waiting for a session.
   843  		//
   844  		// It could be in this states:
   845  		//   1) Reached the select code and awaiting for a value in channel.
   846  		//   2) Reached the select code but already in branch of deadline
   847  		//   cancellation. In this case it is locked on c.mu.Lock().
   848  		//   3) Not reached the select code and thus not reading yet from the
   849  		//   channel.
   850  		//
   851  		// For cases (2) and (3) we close the channel to signal that goroutine
   852  		// missed something and may want to retry (especially for case (3)).
   853  		//
   854  		// After that we taking a next waiter and repeat the same.
   855  		ch := c.waitQ.Remove(el).(*chan *session)
   856  		select {
   857  		case *ch <- s:
   858  			// Case (1).
   859  			return true
   860  
   861  		case <-c.done:
   862  			// Case (2) or (3).
   863  			close(*ch)
   864  
   865  		default:
   866  			// Case (2) or (3).
   867  			close(*ch)
   868  		}
   869  	}
   870  
   871  	return false
   872  }
   873  
   874  func (c *Client) internalPoolSyncCloseSession(ctx context.Context, s *session) {
   875  	var cancel context.CancelFunc
   876  	ctx, cancel = xcontext.WithTimeout(ctx, c.config.DeleteTimeout())
   877  	defer cancel()
   878  
   879  	_ = s.Close(ctx)
   880  }
   881  
   882  // c.mu must be held.
   883  func (c *Client) internalPoolRemoveIdle(s *session) sessionInfo {
   884  	info, has := c.index[s]
   885  	if !has || info.idle == nil {
   886  		panic("inconsistent session client index")
   887  	}
   888  
   889  	c.idle.Remove(info.idle)
   890  	info.idle = nil
   891  	c.index[s] = info
   892  
   893  	return info
   894  }
   895  
   896  // c.mu must be held.
   897  func (c *Client) internalPoolPushIdle(s *session, now time.Time) {
   898  	c.internalPoolHandlePushIdle(s, now, c.idle.PushBack(s))
   899  }
   900  
   901  // c.mu must be held.
   902  func (c *Client) internalPoolHandlePushIdle(s *session, now time.Time, el *list.Element) {
   903  	info, has := c.index[s]
   904  	if !has {
   905  		panic("trying to store session created outside of the client")
   906  	}
   907  	if info.idle != nil {
   908  		panic("inconsistent session client index")
   909  	}
   910  
   911  	info.touched = now
   912  	info.idle = el
   913  	c.index[s] = info
   914  }
   915  
   916  type sessionInfo struct {
   917  	idle    *list.Element
   918  	touched time.Time
   919  }