-
Notifications
You must be signed in to change notification settings - Fork 0
Unbounded Pools
UnboundedPool<TPoolItem> is a variant of the pool that never blocks a lease. The default pool borrows: it holds a hard MaxSize ceiling, and a lease waits when every item is out. The unbounded pool transfers ownership: LeaseAsync returns immediately — handing back an idle item or creating one on the spot — and Release is an optional optimization that donates an item back for reuse.
It's modeled on System.Buffers.ArrayPool<T>: rent freely, return when you can, and a return you skip costs reuse but never correctness. You inject the same IPool<TPoolItem> surface as the bounded pool, so the methods and counters on this page match the IPool API; what changes is the contract behind them.
Reach for the unbounded pool when you want pooled reuse without a concurrency ceiling — when blocking or timing out a caller costs more than allocating one more item. Cap memory by how many idle items you retain (MaxIdle) rather than by how many can be live at once.
Stay with the bounded pool when you need MaxSize as a throttle — when the point of the pool is to limit how many items exist or how many operations run concurrently. The unbounded pool has no such limit.
AddUnboundedPool parallels AddPool. It binds UnboundedPoolOptions from the "UnboundedPoolOptions" configuration section, then applies your configure action:
using Pool;
services.AddTransient<SmtpConnection>();
services.AddUnboundedPool<SmtpConnection>(configuration, options =>
{
options.MinSize = 2; // pre-create two on startup
options.MaxIdle = 50; // retain up to fifty idle items for reuse
options.UseDefaultFactory = true; // construct items from the service provider
});Named pools and typed clients have unbounded counterparts: AddNamedUnboundedPool<TPoolItem>(name, …) registers a keyed pool, and AddUnboundedPool<TPoolItem, TClient>(…) registers a pool keyed by a client type and injects it into that client. Both mirror their bounded equivalents. See Dependency injection for the signatures.
Lease and release through IPool<TPoolItem> exactly as you would a bounded pool:
var connection = await pool.LeaseAsync(cancellationToken);
try
{
await connection.SendAsync(message);
}
finally
{
pool.Release(connection);
}A scoped lease reads the same and is still the recommended style — using donates the item back on scope exit:
using var lease = await pool.LeaseScopeAsync(cancellationToken);
await lease.Item.SendAsync(message, cancellationToken);What differs is what the methods promise.
A lease returns an item right away: an idle one from the pool, or a freshly created one when the pool is empty. There is no capacity gate, so a lease never blocks and never times out. UnboundedPoolOptions has no LeaseTimeout for that reason. The cancellationToken still cancels an already-canceled request up front and cancels preparation, so a PrepareAsync that hangs is still bounded by PreparationTimeout.
Releasing returns ownership of the item to the pool, which keeps it for reuse — but only up to MaxIdle. A return past that cap is dropped, and disposed when the item is IDisposable. Because the pool reuses only what you return, skipping a release costs reuse, not correctness: the item you keep is yours to use or dispose, as any object you own would be.
That ownership transfer is what makes optional release safe even for disposable items. Releasing remains good hygiene — it's how items get reused — so prefer a scoped lease and let using handle it.
The pool disposes only the idle items it still holds:
- a returned item that overflows
MaxIdle, - an item evicted for exceeding
IdleTimeout, discarded the next time a lease meets it, - every idle item on
Clear, before the pool refills toMinSize, - every idle item on
Dispose.
Items out on lease belong to their callers. The pool can't reclaim them, so disposing them is the caller's responsibility — the same rule as the bounded pool, covered in Avoiding footguns.
The counters on IPool<TPoolItem> carry over, with two that read differently because a lease never queues:
-
QueuedLeasesis always0. -
ItemsAllocatedisActiveLeases + ItemsAvailable, whereActiveLeasescounts items leased and not yet returned — including any a caller dropped without releasing, because those items are still in use.
The pool publishes the same metrics instruments under the "MSL.Pool" meter, tagged with its own pool.name of the form "{typeof(TPoolItem).Name}.UnboundedPool". The pool.leases.queued instrument therefore reports 0 for an unbounded pool.
-
Pool options —
UnboundedPoolOptionsin full. -
Dependency injection — the
AddUnboundedPoolextensions. - Avoiding footguns — the release contract and what dispose reclaims.