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 }