Skip to content

How Leasing Works

Mark Lauter edited this page Jun 15, 2026 · 1 revision

How leasing works

This page is for contributors and readers curious about the pool's internals. It traces a single lease from request to hand-off. To use the pool, you need only Leasing and releasing; read on to understand what happens behind that call.

The pool holds two things: a queue of idle items and a capacity gate. The gate is a SemaphoreSlim with one permit per unit of MaxSize. A lease takes a permit; a release returns one. Permits held equals items leased, so the leased count never exceeds MaxSize. The capacity gate covers that design; this page follows the path a lease takes through it.

The lease path

Every lease runs the same sequence:

LeaseAsync
   │
   ├─ permit free? ──no──► wait for a release (up to LeaseTimeout) ──elapsed──► TaskCanceledException
   │       │ yes
   ▼       ▼
 take a permit
   │
   ├─ idle item queued? ──yes──► reuse it (discard first if past IdleTimeout)
   │                    ──no───► create one with the item factory
   ▼
 prepare the item (readiness check, if a strategy is registered)
   │
   ├─ ready ─────► hand it to the caller
   └─ not ready ─► prepare; on failure dispose the item, release the permit, throw

Taking a permit

The pool first tries to take a permit without waiting — a non-blocking probe. When a permit is free, the lease proceeds immediately; this is the fast, uncontended path, and it does not count as a queued lease.

When every permit is held, the lease enters the slow path: it increments the queued-lease counter and awaits the gate, bounded by LeaseTimeout and linked to the caller's cancellation token. When every permit is held, further leases wait in the semaphore's own queue until an item is released or the timeout elapses. That wait queue is the backlog — there is no separate waiter list to fall out of sync with the idle queue.

A wait that the timeout cancels surfaces as TaskCanceledException; a wait the caller's token cancels surfaces as OperationCanceledException.

Acquiring an item

Once the lease holds a permit, it is entitled to exactly one item. The pool dequeues from the idle queue, judging each candidate against a single clock read:

  • An item within IdleTimeout is reused.
  • An item past IdleTimeout is disposed, and the scan continues to the next. Idle eviction is lazy — it happens here, when a lease meets a stale item, not on a background timer.
  • When the queue drains without a live item, the pool creates one with the item factory.

Preparing and handing over

When a preparation strategy is registered, the pool calls IsReadyAsync, and PrepareAsync when the item reports not ready, bounded by PreparationTimeout. A ready item is handed to the caller. When preparation throws, the pool disposes the item, releases the permit, and rethrows — the caller retries for a fresh item.

The permit is the invariant that holds the path together: from the moment it is taken, every exit either releases it exactly once on failure or transfers ownership to the caller on success, who returns it with a later Release. That guarantee is the subject of The capacity gate.

Clone this wiki locally