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