github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/pool/pool.go (about) 1 package pool 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/jonboulle/clockwork" 10 11 "github.com/ydb-platform/ydb-go-sdk/v3/internal/stack" 12 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" 13 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" 14 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xlist" 15 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync" 16 "github.com/ydb-platform/ydb-go-sdk/v3/retry" 17 ) 18 19 type ( 20 Item interface { 21 IsAlive() bool 22 Close(ctx context.Context) error 23 } 24 ItemConstraint[T any] interface { 25 *T 26 Item 27 } 28 Config[PT ItemConstraint[T], T any] struct { 29 trace *Trace 30 clock clockwork.Clock 31 limit int 32 createTimeout time.Duration 33 createItem func(ctx context.Context) (PT, error) 34 closeTimeout time.Duration 35 closeItem func(ctx context.Context, item PT) 36 idleTimeToLive time.Duration 37 itemUsageLimit uint64 38 } 39 itemInfo[PT ItemConstraint[T], T any] struct { 40 idle *xlist.Element[PT] 41 lastUsage time.Time 42 useCounter *uint64 43 } 44 waitChPool[PT ItemConstraint[T], T any] interface { 45 GetOrNew() *chan PT 46 Put(t *chan PT) 47 } 48 Pool[PT ItemConstraint[T], T any] struct { 49 config Config[PT, T] 50 51 createItem func(ctx context.Context) (PT, error) 52 closeItem func(ctx context.Context, item PT) 53 54 mu xsync.RWMutex 55 createInProgress int // KIKIMR-9163: in-create-process counter 56 index map[PT]itemInfo[PT, T] 57 idle xlist.List[PT] 58 waitQ xlist.List[*chan PT] 59 waitChPool waitChPool[PT, T] 60 61 done chan struct{} 62 } 63 Option[PT ItemConstraint[T], T any] func(c *Config[PT, T]) 64 ) 65 66 func WithCreateItemFunc[PT ItemConstraint[T], T any](f func(ctx context.Context) (PT, error)) Option[PT, T] { 67 return func(c *Config[PT, T]) { 68 c.createItem = f 69 } 70 } 71 72 func WithSyncCloseItem[PT ItemConstraint[T], T any]() Option[PT, T] { 73 return func(c *Config[PT, T]) { 74 c.closeItem = func(ctx context.Context, item PT) { 75 _ = item.Close(ctx) 76 } 77 } 78 } 79 80 func WithCreateItemTimeout[PT ItemConstraint[T], T any](t time.Duration) Option[PT, T] { 81 return func(c *Config[PT, T]) { 82 c.createTimeout = t 83 } 84 } 85 86 func WithCloseItemTimeout[PT ItemConstraint[T], T any](t time.Duration) Option[PT, T] { 87 return func(c *Config[PT, T]) { 88 c.closeTimeout = t 89 } 90 } 91 92 func WithLimit[PT ItemConstraint[T], T any](size int) Option[PT, T] { 93 return func(c *Config[PT, T]) { 94 c.limit = size 95 } 96 } 97 98 func WithItemUsageLimit[PT ItemConstraint[T], T any](itemUsageLimit uint64) Option[PT, T] { 99 return func(c *Config[PT, T]) { 100 c.itemUsageLimit = itemUsageLimit 101 } 102 } 103 104 func WithTrace[PT ItemConstraint[T], T any](t *Trace) Option[PT, T] { 105 return func(c *Config[PT, T]) { 106 c.trace = t 107 } 108 } 109 110 func WithIdleTimeToLive[PT ItemConstraint[T], T any](idleTTL time.Duration) Option[PT, T] { 111 return func(c *Config[PT, T]) { 112 c.idleTimeToLive = idleTTL 113 } 114 } 115 116 func WithClock[PT ItemConstraint[T], T any](clock clockwork.Clock) Option[PT, T] { 117 return func(c *Config[PT, T]) { 118 c.clock = clock 119 } 120 } 121 122 func New[PT ItemConstraint[T], T any]( 123 ctx context.Context, 124 opts ...Option[PT, T], 125 ) *Pool[PT, T] { 126 p := &Pool[PT, T]{ 127 config: Config[PT, T]{ 128 trace: &Trace{}, 129 clock: clockwork.NewRealClock(), 130 limit: DefaultLimit, 131 createItem: defaultCreateItem[T, PT], 132 createTimeout: defaultCreateTimeout, 133 closeTimeout: defaultCloseTimeout, 134 }, 135 index: make(map[PT]itemInfo[PT, T]), 136 idle: xlist.New[PT](), 137 waitQ: xlist.New[*chan PT](), 138 waitChPool: &xsync.Pool[chan PT]{ 139 New: func() *chan PT { 140 ch := make(chan PT) 141 142 return &ch 143 }, 144 }, 145 done: make(chan struct{}), 146 } 147 148 for _, opt := range opts { 149 if opt != nil { 150 opt(&p.config) 151 } 152 } 153 154 if onNew := p.config.trace.OnNew; onNew != nil { 155 onDone := onNew(&ctx, 156 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.New"), 157 ) 158 if onDone != nil { 159 defer func() { 160 onDone(p.config.limit) 161 }() 162 } 163 } 164 165 p.createItem = makeAsyncCreateItemFunc(p) 166 if p.config.closeItem != nil { 167 p.closeItem = p.config.closeItem 168 } else { 169 p.closeItem = makeAsyncCloseItemFunc[PT, T](p) 170 } 171 172 return p 173 } 174 175 // defaultCreateItem returns a new item 176 func defaultCreateItem[T any, PT ItemConstraint[T]](context.Context) (PT, error) { 177 var item T 178 179 return &item, nil 180 } 181 182 // makeAsyncCreateItemFunc wraps the createItem function with timeout handling 183 func makeAsyncCreateItemFunc[PT ItemConstraint[T], T any]( //nolint:funlen 184 p *Pool[PT, T], 185 ) func(ctx context.Context) (PT, error) { 186 return func(ctx context.Context) (PT, error) { 187 if !xsync.WithLock(&p.mu, func() bool { 188 if len(p.index)+p.createInProgress < p.config.limit { 189 p.createInProgress++ 190 191 return true 192 } 193 194 return false 195 }) { 196 return nil, xerrors.WithStackTrace(errPoolIsOverflow) 197 } 198 defer func() { 199 p.mu.WithLock(func() { 200 p.createInProgress-- 201 }) 202 }() 203 204 var ( 205 ch = make(chan struct { 206 item PT 207 err error 208 }) 209 done = make(chan struct{}) 210 ) 211 212 defer close(done) 213 214 go func() { 215 defer close(ch) 216 217 createCtx, cancelCreate := xcontext.WithDone(xcontext.ValueOnly(ctx), p.done) 218 defer cancelCreate() 219 220 if d := p.config.createTimeout; d > 0 { 221 createCtx, cancelCreate = xcontext.WithTimeout(createCtx, d) 222 defer cancelCreate() 223 } 224 225 newItem, err := p.config.createItem(createCtx) 226 if newItem != nil { 227 p.mu.WithLock(func() { 228 var useCounter uint64 229 p.index[newItem] = itemInfo[PT, T]{ 230 lastUsage: p.config.clock.Now(), 231 useCounter: &useCounter, 232 } 233 }) 234 } 235 236 select { 237 case ch <- struct { 238 item PT 239 err error 240 }{ 241 item: newItem, 242 err: xerrors.WithStackTrace(err), 243 }: 244 case <-done: 245 if newItem == nil { 246 return 247 } 248 249 _ = p.putItem(createCtx, newItem) 250 } 251 }() 252 253 select { 254 case <-p.done: 255 return nil, xerrors.WithStackTrace(errClosedPool) 256 case <-ctx.Done(): 257 return nil, xerrors.WithStackTrace(ctx.Err()) 258 case result, has := <-ch: 259 if !has { 260 return nil, xerrors.WithStackTrace(xerrors.Retryable(errNoProgress)) 261 } 262 263 if result.err != nil { 264 if xerrors.IsContextError(result.err) { 265 return nil, xerrors.WithStackTrace(xerrors.Retryable(result.err)) 266 } 267 268 return nil, xerrors.WithStackTrace(result.err) 269 } 270 271 return result.item, nil 272 } 273 } 274 } 275 276 func (p *Pool[PT, T]) stats() Stats { 277 return Stats{ 278 Limit: p.config.limit, 279 Index: len(p.index), 280 Idle: p.idle.Len(), 281 Wait: p.waitQ.Len(), 282 CreateInProgress: p.createInProgress, 283 } 284 } 285 286 func (p *Pool[PT, T]) Stats() Stats { 287 p.mu.RLock() 288 defer p.mu.RUnlock() 289 290 return p.stats() 291 } 292 293 func makeAsyncCloseItemFunc[PT ItemConstraint[T], T any]( 294 p *Pool[PT, T], 295 ) func(ctx context.Context, item PT) { 296 return func(ctx context.Context, item PT) { 297 closeItemCtx, closeItemCancel := xcontext.WithDone(xcontext.ValueOnly(ctx), p.done) 298 defer closeItemCancel() 299 300 if d := p.config.closeTimeout; d > 0 { 301 closeItemCtx, closeItemCancel = xcontext.WithTimeout(ctx, d) 302 defer closeItemCancel() 303 } 304 305 go func() { 306 _ = item.Close(closeItemCtx) 307 }() 308 } 309 } 310 311 func (p *Pool[PT, T]) changeState(changeState func() Stats) { 312 if stats, onChange := changeState(), p.config.trace.OnChange; onChange != nil { 313 onChange(stats) 314 } 315 } 316 317 func (p *Pool[PT, T]) try(ctx context.Context, f func(ctx context.Context, item PT) error) (finalErr error) { 318 if onTry := p.config.trace.OnTry; onTry != nil { 319 onDone := onTry(&ctx, 320 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.(*Pool).try"), 321 ) 322 if onDone != nil { 323 defer func() { 324 onDone(finalErr) 325 }() 326 } 327 } 328 329 select { 330 case <-p.done: 331 return xerrors.WithStackTrace(errClosedPool) 332 case <-ctx.Done(): 333 return xerrors.WithStackTrace(ctx.Err()) 334 default: 335 } 336 337 item, err := p.getItem(ctx) 338 if err != nil { 339 if xerrors.IsYdb(err) { 340 return xerrors.WithStackTrace(xerrors.Retryable(err)) 341 } 342 343 return xerrors.WithStackTrace(err) 344 } 345 346 defer func() { 347 _ = p.putItem(ctx, item) 348 }() 349 350 err = f(ctx, item) 351 if err != nil { 352 return xerrors.WithStackTrace(err) 353 } 354 355 return nil 356 } 357 358 func (p *Pool[PT, T]) With( 359 ctx context.Context, 360 f func(ctx context.Context, item PT) error, 361 opts ...retry.Option, 362 ) (finalErr error) { 363 var attempts int 364 365 if onWith := p.config.trace.OnWith; onWith != nil { 366 onDone := onWith(&ctx, 367 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.(*Pool).With"), 368 ) 369 if onDone != nil { 370 defer func() { 371 onDone(attempts, finalErr) 372 }() 373 } 374 } 375 376 err := retry.Retry(ctx, func(ctx context.Context) error { 377 attempts++ 378 err := p.try(ctx, f) 379 if err != nil { 380 return xerrors.WithStackTrace(err) 381 } 382 383 return nil 384 }, opts...) 385 if err != nil { 386 return xerrors.WithStackTrace(fmt.Errorf("pool.With failed with %d attempts: %w", attempts, err)) 387 } 388 389 return nil 390 } 391 392 func (p *Pool[PT, T]) Close(ctx context.Context) (finalErr error) { 393 if onClose := p.config.trace.OnClose; onClose != nil { 394 onDone := onClose(&ctx, 395 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.(*Pool).Close"), 396 ) 397 if onDone != nil { 398 defer func() { 399 onDone(finalErr) 400 }() 401 } 402 } 403 404 select { 405 case <-p.done: 406 return xerrors.WithStackTrace(errClosedPool) 407 408 default: 409 close(p.done) 410 411 p.mu.Lock() 412 defer p.mu.Unlock() 413 414 p.changeState(func() Stats { 415 p.config.limit = 0 416 417 for el := p.waitQ.Front(); el != nil; el = el.Next() { 418 close(*el.Value) 419 } 420 421 p.waitQ.Clear() 422 423 var wg sync.WaitGroup 424 wg.Add(p.idle.Len()) 425 426 for el := p.idle.Front(); el != nil; el = el.Next() { 427 go func(item PT) { 428 defer wg.Done() 429 p.closeItem(ctx, item) 430 }(el.Value) 431 delete(p.index, el.Value) 432 } 433 434 wg.Wait() 435 436 p.idle.Clear() 437 438 return p.stats() 439 }) 440 441 return nil 442 } 443 } 444 445 // getWaitCh returns pointer to a channel of sessions. 446 // 447 // Note that returning a pointer reduces allocations on sync.Pool usage – 448 // sync.Client.Get() returns empty interface, which leads to allocation for 449 // non-pointer values. 450 func (p *Pool[PT, T]) getWaitCh() *chan PT { //nolint:gocritic 451 return p.waitChPool.GetOrNew() 452 } 453 454 // putWaitCh receives pointer to a channel and makes it available for further 455 // use. 456 // Note that ch MUST NOT be owned by any goroutine at the call moment and ch 457 // MUST NOT contain any value. 458 func (p *Pool[PT, T]) putWaitCh(ch *chan PT) { //nolint:gocritic 459 p.waitChPool.Put(ch) 460 } 461 462 // p.mu must be held. 463 func (p *Pool[PT, T]) peekFirstIdle() (item PT, touched time.Time) { 464 el := p.idle.Front() 465 if el == nil { 466 return 467 } 468 item = el.Value 469 info, has := p.index[item] 470 if !has || el != info.idle { 471 panic(fmt.Sprintf("inconsistent index: (%v, %+v, %+v)", has, el, info.idle)) 472 } 473 474 return item, info.lastUsage 475 } 476 477 // removes first session from idle and resets the keepAliveCount 478 // to prevent session from dying in the internalPoolGC after it was returned 479 // to be used only in outgoing functions that make session busy. 480 // p.mu must be held. 481 func (p *Pool[PT, T]) removeFirstIdle() PT { 482 idle, _ := p.peekFirstIdle() 483 if idle != nil { 484 info := p.removeIdle(idle) 485 p.index[idle] = info 486 } 487 488 return idle 489 } 490 491 // p.mu must be held. 492 func (p *Pool[PT, T]) notifyAboutIdle(idle PT) (notified bool) { 493 for el := p.waitQ.Front(); el != nil; el = p.waitQ.Front() { 494 // Some goroutine is waiting for a session. 495 // 496 // It could be in this states: 497 // 1) Reached the select code and awaiting for a value in channel. 498 // 2) Reached the select code but already in branch of deadline 499 // cancellation. In this case it is locked on p.mu.Lock(). 500 // 3) Not reached the select code and thus not reading yet from the 501 // channel. 502 // 503 // For cases (2) and (3) we close the channel to signal that goroutine 504 // missed something and may want to retry (especially for case (3)). 505 // 506 // After that we taking a next waiter and repeat the same. 507 var ch *chan PT 508 p.changeState(func() Stats { 509 ch = p.waitQ.Remove(el) //nolint:scopelint 510 511 return p.stats() 512 }) 513 select { 514 case *ch <- idle: 515 // Case (1). 516 return true 517 518 case <-p.done: 519 // Case (2) or (3). 520 close(*ch) 521 522 default: 523 // Case (2) or (3). 524 close(*ch) 525 } 526 } 527 528 return false 529 } 530 531 // p.mu must be held. 532 func (p *Pool[PT, T]) removeIdle(item PT) itemInfo[PT, T] { 533 info, has := p.index[item] 534 if !has || info.idle == nil { 535 panic("inconsistent session client index") 536 } 537 538 p.changeState(func() Stats { 539 p.idle.Remove(info.idle) 540 info.idle = nil 541 p.index[item] = info 542 543 return p.stats() 544 }) 545 546 return info 547 } 548 549 // p.mu must be held. 550 func (p *Pool[PT, T]) pushIdle(item PT, now time.Time) { 551 info, has := p.index[item] 552 if !has { 553 panic("trying to store item created outside of the client") 554 } 555 if info.idle != nil { 556 panic("inconsistent item client index") 557 } 558 559 p.changeState(func() Stats { 560 info.lastUsage = now 561 info.idle = p.idle.PushBack(item) 562 p.index[item] = info 563 564 return p.stats() 565 }) 566 } 567 568 const maxAttempts = 100 569 570 func (p *Pool[PT, T]) getItem(ctx context.Context) (item PT, finalErr error) { //nolint:funlen 571 var ( 572 start = p.config.clock.Now() 573 attempt int 574 lastErr error 575 ) 576 577 if onGet := p.config.trace.OnGet; onGet != nil { 578 onDone := onGet(&ctx, 579 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.(*Pool).getItem"), 580 ) 581 if onDone != nil { 582 defer func() { 583 onDone(item, attempt, finalErr) 584 }() 585 } 586 } 587 588 for ; attempt < maxAttempts; attempt++ { 589 select { 590 case <-p.done: 591 return nil, xerrors.WithStackTrace(errClosedPool) 592 default: 593 } 594 595 if item := xsync.WithLock(&p.mu, func() PT { //nolint:nestif 596 return p.removeFirstIdle() 597 }); item != nil { 598 if item.IsAlive() { 599 info := xsync.WithLock(&p.mu, func() itemInfo[PT, T] { 600 info, has := p.index[item] 601 if !has { 602 panic("no index for item") 603 } 604 605 *info.useCounter++ 606 607 return info 608 }) 609 610 if (p.config.itemUsageLimit > 0 && *info.useCounter > p.config.itemUsageLimit) || 611 (p.config.idleTimeToLive > 0 && p.config.clock.Since(info.lastUsage) > p.config.idleTimeToLive) { 612 p.closeItem(ctx, item) 613 p.mu.WithLock(func() { 614 p.changeState(func() Stats { 615 delete(p.index, item) 616 617 return p.stats() 618 }) 619 }) 620 621 continue 622 } 623 624 return item, nil 625 } 626 } 627 628 item, err := p.createItem(ctx) 629 if item != nil { 630 return item, nil 631 } 632 633 if !isRetriable(err) { 634 return nil, xerrors.WithStackTrace(xerrors.Join(err, lastErr)) 635 } 636 637 lastErr = err 638 639 item, err = p.waitFromCh(ctx) 640 if item != nil { 641 return item, nil 642 } 643 644 if err != nil && !isRetriable(err) { 645 return nil, xerrors.WithStackTrace(xerrors.Join(err, lastErr)) 646 } 647 648 lastErr = err 649 } 650 651 p.mu.RLock() 652 defer p.mu.RUnlock() 653 654 return nil, xerrors.WithStackTrace( 655 fmt.Errorf("failed to get item from pool after %d attempts and %v, pool has %d items (%d busy, "+ 656 "%d idle, %d create_in_progress): %w", attempt, p.config.clock.Since(start), len(p.index), 657 len(p.index)-p.idle.Len(), p.idle.Len(), p.createInProgress, lastErr, 658 ), 659 ) 660 } 661 662 //nolint:funlen 663 func (p *Pool[PT, T]) waitFromCh(ctx context.Context) (item PT, finalErr error) { 664 var ( 665 ch *chan PT 666 el *xlist.Element[*chan PT] 667 ) 668 669 p.mu.WithLock(func() { 670 p.changeState(func() Stats { 671 ch = p.getWaitCh() 672 el = p.waitQ.PushBack(ch) 673 674 return p.stats() 675 }) 676 }) 677 678 if onWait := p.config.trace.onWait; onWait != nil { 679 onDone := onWait() 680 if onDone != nil { 681 defer func() { 682 onDone(item, finalErr) 683 }() 684 } 685 } 686 687 var deadliine <-chan time.Time 688 if timeout := p.config.createTimeout; timeout > 0 { 689 t := p.config.clock.NewTimer(timeout) 690 defer t.Stop() 691 692 deadliine = t.Chan() 693 } 694 695 select { 696 case <-p.done: 697 p.mu.WithLock(func() { 698 p.changeState(func() Stats { 699 p.waitQ.Remove(el) 700 701 return p.stats() 702 }) 703 }) 704 705 return nil, xerrors.WithStackTrace(errClosedPool) 706 707 case item, ok := <-*ch: 708 // Note that race may occur and some goroutine may try to write 709 // session into channel after it was enqueued but before it being 710 // read here. In that case we will receive nil here and will retry. 711 // 712 // The same way will work when some session become deleted - the 713 // nil value will be sent into the channel. 714 if ok { 715 // Put only filled and not closed channel back to the Client. 716 // That is, we need to avoid races on filling reused channel 717 // for the next waiter – session could be lost for a long time. 718 p.putWaitCh(ch) 719 } 720 721 return item, nil 722 723 case <-deadliine: 724 p.mu.WithLock(func() { 725 p.changeState(func() Stats { 726 p.waitQ.Remove(el) 727 728 return p.stats() 729 }) 730 }) 731 732 return nil, nil 733 734 case <-ctx.Done(): 735 p.mu.WithLock(func() { 736 p.changeState(func() Stats { 737 p.waitQ.Remove(el) 738 739 return p.stats() 740 }) 741 }) 742 743 return nil, xerrors.WithStackTrace(ctx.Err()) 744 } 745 } 746 747 // p.mu must be free. 748 func (p *Pool[PT, T]) putItem(ctx context.Context, item PT) (finalErr error) { 749 if onPut := p.config.trace.OnPut; onPut != nil { 750 onDone := onPut(&ctx, 751 stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/internal/pool.(*Pool).putItem"), 752 item, 753 ) 754 if onDone != nil { 755 defer func() { 756 onDone(finalErr) 757 }() 758 } 759 } 760 select { 761 case <-p.done: 762 p.closeItem(ctx, item) 763 p.mu.WithLock(func() { 764 p.changeState(func() Stats { 765 delete(p.index, item) 766 767 return p.stats() 768 }) 769 }) 770 771 return xerrors.WithStackTrace(errClosedPool) 772 default: 773 p.mu.Lock() 774 defer p.mu.Unlock() 775 776 if !item.IsAlive() { 777 p.closeItem(ctx, item) 778 p.changeState(func() Stats { 779 delete(p.index, item) 780 781 return p.stats() 782 }) 783 784 return xerrors.WithStackTrace(errItemIsNotAlive) 785 } 786 787 if p.idle.Len() >= p.config.limit { 788 p.closeItem(ctx, item) 789 p.changeState(func() Stats { 790 delete(p.index, item) 791 792 return p.stats() 793 }) 794 795 return xerrors.WithStackTrace(errPoolIsOverflow) 796 } 797 798 if !p.notifyAboutIdle(item) { 799 p.pushIdle(item, p.config.clock.Now()) 800 } 801 802 return nil 803 } 804 }