/**
* Shares scoped resources across fibers.
*
* A `Pool` acquires resource-backed values with a scoped effect, lets
* fibers borrow them with `get`, can invalidate broken values, and releases all
* acquired values when the pool scope closes. This module includes fixed-size
* pools, pools that resize with a time-to-live policy, custom strategy pools,
* per-item concurrency limits, and runtime state types used by pool strategies.
*
* @stability stable
* @since 2.0.0
*/
import type * as Cause from "./Cause.ts"
import { Clock } from "./Clock.ts"
import * as Context from "./Context.ts"
import * as Duration from "./Duration.ts"
import * as Effect from "./Effect.ts"
import type * as Exit from "./Exit.ts"
import * as Fiber from "./Fiber.ts"
import { constant, dual, identity } from "./Function.ts"
import * as core from "./internal/core.ts"
import * as internal from "./internal/effect.ts"
import { type Pipeable, pipeArguments } from "./Pipeable.ts"
import { hasProperty } from "./Predicate.ts"
import * as Queue from "./Queue.ts"
import { UnhandledLogLevel } from "./References.ts"
import * as Scope from "./Scope.ts"
import * as Semaphore from "./Semaphore.ts"
const TypeId = "~effect/Pool"
const Acquire = Symbol()
const AcquireContext = Symbol()
const PoolClock = Symbol()
interface PoolImpl extends Pool {
readonly [Acquire]: Effect.Effect
readonly [AcquireContext]: Context.Context
readonly [PoolClock]: Clock
}
/**
* A `Pool` is a pool of items of type `A`, each of which may be
* associated with the acquisition and release of resources. An attempt to get
* an item `A` from a pool may fail with an error of type `E`.
*
* **When to use**
*
* Use when you need to share a bounded set of scoped resources across fibers
* while the pool manages acquisition, reuse, and release.
*
* @see {@link make} for creating a pool with size bounds
* @see {@link makeWithTTL} for creating a pool with idle item expiration
* @see {@link makeWithStrategy} for creating a pool with a custom strategy
* @see {@link get} for acquiring an item from a pool
* @see {@link invalidate} for removing a broken item from the pool
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Pool extends Pipeable {
readonly [TypeId]: typeof TypeId
readonly config: Config
readonly state: State
}
/**
* Normalized configuration used by a `Pool`.
*
* **When to use**
*
* Use as the normalized, read-only description of how a pool acquires, sizes,
* shares, and resizes its items after construction.
*
* **Details**
*
* The config stores the acquire effect, size bounds, per-item concurrency,
* target utilization, and resizing strategy used by the pool implementation.
*
* @see {@link Pool} for the value exposing this configuration
* @see {@link State} for mutable runtime state instead of static configuration
* @see {@link Strategy} for the resizing and reclamation contract stored on the config
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface Config {
readonly acquire: Effect.Effect
readonly concurrency: number
readonly isFixed: boolean
readonly minSize: number
readonly maxSize: number
readonly strategy: Strategy
readonly targetUtilization: number
}
/**
* Mutable runtime state maintained by a `Pool`.
*
* **When to use**
*
* Use when you need to inspect or support the runtime state backing a `Pool`,
* including its scope, item sets, semaphores, waiters, invalidation tracking,
* and shutdown flag.
*
* **Details**
*
* This state is exposed for inspection and implementation support. User code
* should prefer the high-level pool operations.
*
* @see {@link Pool} for the pool value exposing this state
* @see {@link PoolItem} for the entries stored in the runtime item sets
* @see {@link get} for acquiring items through the high-level API
* @see {@link invalidate} for invalidating items through the high-level API
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface State {
readonly scope: Scope.Scope
isShuttingDown: boolean
usage: number
readonly resizeSemaphore: Semaphore.Semaphore
readonly items: Set>
availableHead: PoolItem | undefined
availableTail: PoolItem | undefined
readonly invalidated: Set>
readonly waiters: Set<(failure?: Exit.Failure) => void>
}
/**
* Internal record for a value managed by a `Pool`.
*
* **When to use**
*
* Use when implementing a custom pool `Strategy` that needs to inspect
* acquired items, track reference counts, or return reclaimable items to the
* pool.
*
* **Details**
*
* Each item stores the acquisition `Exit`, its finalizer, the current
* reference count, when it last became idle, and whether automatic reclaiming
* has been disabled because the item was invalidated.
*
* @see {@link Strategy} for the custom strategy callbacks that receive and return pool items
* @see {@link State} for the runtime sets that store active, available, and invalidated pool items
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface PoolItem {
readonly exit: Exit.Exit
finalizer: Effect.Effect
refCount: number
/** Clock time at which the item was acquired or its last borrower released it. */
idleSince: number
disableReclaim: boolean
isAvailable: boolean
availablePrevious: PoolItem | undefined
availableNext: PoolItem | undefined
release: (exit: Exit.Exit) => Effect.Effect
}
/**
* Strategy used by a `Pool` to manage background resizing and item
* reclamation.
*
* **When to use**
*
* Use when defining a custom pool lifecycle policy that needs to run background
* work, observe acquired items, or choose items for reclamation.
*
* **Details**
*
* `run` starts any strategy-specific background work, `onAcquire` is invoked
* when an item is acquired, and `reclaim` selects an item that can be removed
* or replaced.
*
* @see {@link makeWithStrategy} for constructing a pool from a custom `Strategy`
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface Strategy {
readonly run: (pool: Pool) => Effect.Effect
readonly onAcquire: (item: PoolItem) => Effect.Effect
readonly reclaim: (pool: Pool) => Effect.Effect | undefined>
}
/**
* Returns `true` if the specified value is a `Pool`, `false` otherwise.
*
* **When to use**
*
* Use to validate unknown values at runtime boundaries before treating them as
* `Pool` values.
*
* **Details**
*
* This predicate narrows the input to `Pool`.
*
* @stability stable
* @category guards
* @since 2.0.0
*/
export const isPool = (u: unknown): u is Pool => hasProperty(u, TypeId)
/**
* Makes a new pool of the specified fixed size.
*
* **When to use**
*
* Use when you need a fixed-size pool with no growth or shrinkage.
*
* **Details**
*
* The pool is returned in a `Scope`, which governs the lifetime of the pool.
* When the pool is shutdown because the `Scope` is closed, the individual
* items allocated by the pool will be released in some unspecified order.
*
* By setting the `concurrency` parameter, you can control the level of concurrent
* access per pool item. By default, the number of permits is set to `1`.
*
* `targetUtilization` determines when to create new pool items. It is a value
* between 0 and 1, where 1 means only create new pool items when all the existing
* items are fully utilized.
*
* A `targetUtilization` of 0.5 will create new pool items when the existing items are
* 50% utilized.
*
* @see {@link makeWithTTL} for pools with min/max sizes and a TTL-based shrinking policy
* @see {@link makeWithStrategy} for pools with a custom resizing and reclamation strategy
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const make = (options: {
readonly acquire: Effect.Effect
readonly size: number
readonly concurrency?: number | undefined
readonly targetUtilization?: number | undefined
}): Effect.Effect, never, R | Scope.Scope> =>
makeWithStrategy({ ...options, min: options.size, max: options.size, strategy: strategyNoop })
/**
* Creates a scoped pool with minimum and maximum sizes and a time-to-live
* policy for shrinking unused excess items.
*
* **When to use**
*
* Use to create an elastic scoped pool that can grow up to a maximum size and
* later reclaim unused excess items.
*
* **Details**
*
* The returned pool requires `Scope`; when that scope is closed, allocated
* items are released in an unspecified order. `concurrency` controls how many
* fibers may use each pool item at once and defaults to `1`.
*
* `targetUtilization` controls when new items are created and is clamped by the
* pool implementation. A value of `1` waits until existing items are fully
* utilized before creating more items.
*
* `timeToLiveStrategy` defaults to `"usage"`, which expires idle, unreserved
* excess items after `timeToLive` since their last borrower released them
* (or acquisition if never borrowed). It checks once per `timeToLive`, so
* retirement can take up to twice that duration. `"creation"` measures from
* item creation instead.
*
* **Example** (Creating a connection pool)
*
* ```ts import.meta.vitest
* import { Duration, Effect, Pool } from "effect"
*
* interface Connection {
* readonly execute: (sql: string) => Effect.Effect>
* readonly close: Effect.Effect
* }
*
* const acquireDBConnection = Effect.acquireRelease(
* Effect.succeed({
* execute: (sql) => Effect.succeed([`executed: ${sql}`]),
* close: Effect.void
* } satisfies Connection),
* (connection) => connection.close
* )
*
* const program = Effect.scoped(
* Effect.flatMap(
* Pool.makeWithTTL({
* acquire: acquireDBConnection,
* min: 10,
* max: 20,
* timeToLive: Duration.seconds(60)
* }),
* (pool) => Effect.flatMap(Pool.get(pool), (connection) => connection.execute("select 1"))
* )
* )
*
* await Effect.runPromise(program) // => ["executed: select 1"]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const makeWithTTL = (options: {
readonly acquire: Effect.Effect
readonly min: number
readonly max: number
readonly concurrency?: number | undefined
readonly targetUtilization?: number | undefined
readonly timeToLive: Duration.Input
readonly timeToLiveStrategy?: "creation" | "usage" | undefined
}): Effect.Effect, never, R | Scope.Scope> =>
Effect.flatMap(
options.timeToLiveStrategy === "creation" ?
strategyCreationTTL(options.timeToLive) :
strategyUsageTTL(options.timeToLive),
(strategy) => makeWithStrategy({ ...options, strategy })
)
/**
* Creates a scoped pool using a custom resizing and reclamation strategy.
*
* **When to use**
*
* Use to build a pool whose item lifecycle is controlled by an explicit
* `Strategy`, such as custom background resizing, replacement, or reclamation.
*
* **Details**
*
* The returned pool requires `Scope`; closing the scope shuts down the pool and
* releases allocated items.
*
* @see {@link make} for fixed-size pools without custom resizing or reclamation
* @see {@link makeWithTTL} for min/max pools that shrink excess items with a TTL policy
* @see {@link Strategy} for the custom strategy contract consumed by this constructor
*
* @stability stable
* @category constructors
* @since 4.0.0
*/
export const makeWithStrategy = (options: {
readonly acquire: Effect.Effect
readonly min: number
readonly max: number
readonly concurrency?: number | undefined
readonly targetUtilization?: number | undefined
readonly strategy: Strategy
}): Effect.Effect, never, Scope.Scope | R> =>
Effect.uninterruptibleMask(Effect.fnUntraced(function*(restore) {
const services = yield* Effect.context()
const scope = Context.get(services, Scope.Scope)
const acquire = Effect.updateContext(
options.acquire,
(input) => Context.merge(services, input)
) as Effect.Effect
const concurrency = options.concurrency ?? 1
const config: Config = {
acquire,
concurrency,
isFixed: options.min === options.max,
minSize: options.min,
maxSize: options.max,
strategy: options.strategy,
targetUtilization: Math.min(Math.max(options.targetUtilization ?? 1, 0.1), 1)
}
const state: State = {
scope,
isShuttingDown: false,
usage: 0,
resizeSemaphore: Semaphore.makeUnsafe(1),
items: new Set(),
availableHead: undefined,
availableTail: undefined,
invalidated: new Set(),
waiters: new Set()
}
const self: PoolImpl = {
[TypeId]: TypeId,
[Acquire]: options.acquire as Effect.Effect,
[AcquireContext]: services as Context.Context,
[PoolClock]: yield* Clock,
config,
state,
pipe() {
return pipeArguments(this, arguments)
}
}
yield* Scope.addFinalizer(scope, shutdown(self))
if (config.minSize > 0) {
yield* Effect.tap(
Effect.forkDetach(restore(resize(self)), { startImmediately: true }),
(fiber) => Scope.addFinalizer(scope, Fiber.interrupt(fiber))
)
}
if (options.strategy !== strategyNoop) {
yield* Effect.tap(
Effect.forkDetach(restore(options.strategy.run(self))),
(fiber) => Scope.addFinalizer(scope, Fiber.interrupt(fiber))
)
}
return self
}))
const shutdown = Effect.fnUntraced(function*(self: Pool) {
if (self.state.isShuttingDown) return
self.state.isShuttingDown = true
const size = self.state.items.size
const semaphore = Semaphore.makeUnsafe(size)
for (const item of self.state.items) {
if (item.refCount > 0) {
item.finalizer = Effect.tap(item.finalizer, semaphore.release(1))
self.state.invalidated.add(item)
yield* semaphore.take(1)
} else {
self.state.items.delete(item)
removeAvailable(self, item)
self.state.invalidated.delete(item)
yield* item.finalizer
}
}
if (self.state.waiters.size > 0) {
const waiters = Array.from(self.state.waiters)
self.state.waiters.clear()
for (const notify of waiters) notify()
}
yield* semaphore.take(size)
})
/**
* Retrieves an item from the pool in a scoped effect.
*
* **When to use**
*
* Use to borrow a pooled resource for the lifetime of the current scope so it
* is automatically returned when that scope closes.
*
* **Details**
*
* The returned effect waits for an available item when the pool is at capacity.
* If acquiring a new item fails, the effect fails with the acquisition error.
*
* **Gotchas**
*
* Retrying a failed `get` can repeat the acquisition attempt.
*
* @see {@link invalidate} for removing an unhealthy item from future reuse
*
* @stability stable
* @category getters
* @since 2.0.0
*/
export const get = (self: Pool): Effect.Effect =>
core.withFiber((fiber) => {
const state = self.state
if (state.isShuttingDown) return internal.interrupt
if (state.availableHead?.exit._tag === "Success") {
state.usage++
if (self.config.isFixed || targetSize(self) <= activeSize(self)) {
return leaseItem(self, state.availableHead, fiber)
}
state.usage--
}
return getSlowWith(self, leaseItemWith)
})
/**
* Borrows an item while an effect runs and returns it when the effect exits.
*
* **When to use**
*
* Use when an item is needed by one effect. Unlike `Effect.scoped` with
* {@link get}, this avoids allocating a scope and registering a finalizer.
*
* **Example** (Running a single operation with a pooled item)
*
* ```ts import.meta.vitest
* import { Effect, Pool } from "effect"
*
* const program = Effect.scoped(
* Effect.flatMap(
* Pool.make({ acquire: Effect.succeed("resource"), size: 2 }),
* (pool) => Pool.use(pool, (item) => Effect.succeed(item.length))
* )
* )
*
* await Effect.runPromise(program) // => 8
* ```
*
* @see {@link get} for borrowing an item for the lifetime of a scope
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
export const use: {
/**
* Borrows an item while an effect runs and returns it when the effect exits.
*
* **When to use**
*
* Use when an item is needed by one effect. Unlike `Effect.scoped` with
* {@link get}, this avoids allocating a scope and registering a finalizer.
*
* **Example** (Running a single operation with a pooled item)
*
* ```ts import.meta.vitest
* import { Effect, Pool } from "effect"
*
* const program = Effect.scoped(
* Effect.flatMap(
* Pool.make({ acquire: Effect.succeed("resource"), size: 2 }),
* (pool) => Pool.use(pool, (item) => Effect.succeed(item.length))
* )
* )
*
* await Effect.runPromise(program) // => 8
* ```
*
* @see {@link get} for borrowing an item for the lifetime of a scope
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
(f: (item: A) => Effect.Effect): (self: Pool) => Effect.Effect
/**
* Borrows an item while an effect runs and returns it when the effect exits.
*
* **When to use**
*
* Use when an item is needed by one effect. Unlike `Effect.scoped` with
* {@link get}, this avoids allocating a scope and registering a finalizer.
*
* **Example** (Running a single operation with a pooled item)
*
* ```ts import.meta.vitest
* import { Effect, Pool } from "effect"
*
* const program = Effect.scoped(
* Effect.flatMap(
* Pool.make({ acquire: Effect.succeed("resource"), size: 2 }),
* (pool) => Pool.use(pool, (item) => Effect.succeed(item.length))
* )
* )
*
* await Effect.runPromise(program) // => 8
* ```
*
* @see {@link get} for borrowing an item for the lifetime of a scope
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
(self: Pool, f: (item: A) => Effect.Effect): Effect.Effect
} = dual(2, (
self: Pool,
f: (item: A) => Effect.Effect
): Effect.Effect =>
core.withFiber((fiber) => {
const state = self.state
if (state.isShuttingDown) return internal.interrupt
if (state.availableHead?.exit._tag === "Success") {
state.usage++
if (self.config.isFixed || targetSize(self) <= activeSize(self)) {
return useItem(self, state.availableHead, f, fiber)
}
state.usage--
}
return getSlowWith(self, (self, item, fiber, restore) => useItem(self, item, f, fiber, restore))
}))
// Count the lease and install its release in the same step.
const useItem = (
self: Pool,
item: PoolItem,
f: (item: A) => Effect.Effect,
fiber: Fiber.Fiber,
restore?: (effect: Effect.Effect) => Effect.Effect
): Effect.Effect => {
leaseItemBookkeeping(self, item)
internal.onExitUnsafe(fiber, item.release)
let body: Effect.Effect
try {
body = f((item.exit as Exit.Success).value)
} catch (defect) {
return core.exitDie(defect)
}
return restore !== undefined ? restore(body) : body
}
const getSlowWith = (
self: Pool,
lease: (
self: Pool,
item: PoolItem,
fiber: Fiber.Fiber,
restore: (effect: Effect.Effect) => Effect.Effect
) => Effect.Effect
): Effect.Effect =>
internal.uninterruptibleMask((restore) => {
const state = self.state
state.usage++
const wait: Effect.Effect = internal.flatMap(
internal.onError(
restore(waitForItem(self)),
() =>
internal.sync(() => {
state.usage--
})
),
() => loop
)
const step: Effect.Effect = core.withFiber((fiber) => {
if (state.isShuttingDown) {
state.usage--
return internal.interrupt
}
// Replace failed slots only when the pool cannot grow.
const item = firstHealthyAvailable(self) ??
(targetSize(self) <= activeSize(self) ? state.availableHead : undefined)
if (item === undefined) {
return wait
}
if (item.exit._tag === "Failure") {
removeFailedItem(self, item)
return loop
}
return lease(self, item, fiber, restore)
})
const loop: Effect.Effect = internal.suspend(() => {
if (state.isShuttingDown) {
state.usage--
return internal.interrupt
}
return targetSize(self) > activeSize(self)
? internal.flatMap(
state.resizeSemaphore.withPermitsIfAvailable(1)(
Effect.forkIn(Effect.interruptible(resize(self)), state.scope)
),
() => step
)
: step
})
return loop
})
const leaseItemBookkeeping = (self: Pool, item: PoolItem): void => {
item.refCount++
if (item.refCount >= self.config.concurrency) {
removeAvailable(self, item)
}
}
const removeFailedItem = (self: Pool, item: PoolItem): void => {
self.state.items.delete(item)
self.state.invalidated.delete(item)
removeAvailable(self, item)
}
const leaseItem = (
self: Pool,
item: PoolItem,
fiber: Fiber.Fiber
): Effect.Effect => {
leaseItemBookkeeping(self, item)
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
if (scope.state._tag === "Closed") {
internal.onExitUnsafe(fiber, item.release)
} else {
internal.scopeAddFinalizerUnsafe(scope, {}, item.release)
}
return item.exit
}
const leaseItemWith = (
self: Pool,
item: PoolItem,
fiber: Fiber.Fiber
): Effect.Effect => leaseItem(self, item, fiber)
const releaseItem = (self: Pool, item: PoolItem): Effect.Effect =>
core.withFiber((fiber) => {
const state = self.state
item.refCount--
state.usage--
if (item.refCount === 0) {
item.idleSince = (self as PoolImpl)[PoolClock].currentTimeMillisUnsafe()
}
if (state.invalidated.has(item)) {
return invalidatePoolItem(self, item)
}
// Every release frees one slot, so it can admit one waiter. Reacting only
// to the saturated-to-unsaturated transition strands the rest: several
// leases returning at once would wake a single waiter and leave the others
// asleep against an item that has capacity for them. `addAvailable` is
// idempotent, so re-adding an available item is free.
if (item.refCount < self.config.concurrency) {
addAvailableFront(self, item)
wakeWaiters(self, fiber, 1)
}
return internal.void
})
const firstHealthyAvailable = (self: Pool): PoolItem | undefined => {
let item = self.state.availableHead
while (item !== undefined && item.exit._tag === "Failure") item = item.availableNext
return item
}
const waitForItem = (self: Pool): Effect.Effect =>
internal.callback((resume) => {
const state = self.state
if (firstHealthyAvailable(self) !== undefined || state.isShuttingDown) {
return resume(internal.void)
}
const observer = (failure?: Exit.Failure) => {
state.waiters.delete(observer)
resume(failure ?? internal.void)
}
state.waiters.add(observer)
return internal.sync(() => {
state.waiters.delete(observer)
})
})
const wakeWaiters = (self: Pool, fiber: Fiber.Fiber, count: number) => {
const waiters = self.state.waiters
if (waiters.size === 0) return
fiber.currentDispatcher.scheduleTask(() => {
let remaining = count
const toWake: Array<(failure?: Exit.Failure) => void> = []
for (const notify of waiters) {
if (remaining-- <= 0) break
toWake.push(notify)
}
for (let i = 0; i < toWake.length; i++) {
toWake[i]()
}
}, 0)
}
const wakeAll = (self: Pool): Effect.Effect =>
core.withFiber((fiber) => {
wakeWaiters(self, fiber, Number.POSITIVE_INFINITY)
return internal.void
})
// Reservations prevent reuse without extending the lifetime of borrowed items.
const reservations = new WeakMap, number>()
/** Adds a freshly acquired item, which has no use behind it, at the back. */
const addAvailable = (self: Pool, item: PoolItem): void => {
if (item.isAvailable || reservations.has(item)) return
item.isAvailable = true
item.availablePrevious = self.state.availableTail
item.availableNext = undefined
if (self.state.availableTail !== undefined) {
self.state.availableTail.availableNext = item
} else {
self.state.availableHead = item
}
self.state.availableTail = item
}
/**
* Returns a released item at the front, so the next borrow gets the one used
* most recently. Borrowers take from the front, so the list runs warmest
* first.
*
* Sending it to the back instead spreads a sequence of borrows evenly over
* every item the pool has open. For a pool of connections that means none of
* them is ever the hot one - each borrow lands on a peer that has been sitting
* idle, losing whatever warmth it had - and it means `timeToLive` never
* reclaims anything, because a pool that grew for one burst keeps every item
* equally fresh forever. Under saturation the two orders agree, since every
* item is checked out either way.
*/
const addAvailableFront = (self: Pool, item: PoolItem): void => {
if (item.isAvailable || reservations.has(item)) return
item.isAvailable = true
item.availablePrevious = undefined
item.availableNext = self.state.availableHead
if (self.state.availableHead !== undefined) {
self.state.availableHead.availablePrevious = item
} else {
self.state.availableTail = item
}
self.state.availableHead = item
}
const removeAvailable = (self: Pool, item: PoolItem): void => {
if (!item.isAvailable) return
item.isAvailable = false
if (item.availablePrevious !== undefined) {
item.availablePrevious.availableNext = item.availableNext
} else {
self.state.availableHead = item.availableNext
}
if (item.availableNext !== undefined) {
item.availableNext.availablePrevious = item.availablePrevious
} else {
self.state.availableTail = item.availablePrevious
}
item.availablePrevious = undefined
item.availableNext = undefined
}
/**
* Invalidates the specified item so the pool can remove it and reallocate the
* item, lazily if needed.
*
* **When to use**
*
* Use to prevent a pooled item from being reused after it becomes unsuitable,
* such as a stale connection or a resource that failed a health check.
*
* **Gotchas**
*
* The item is matched with strict equality. Passing an equivalent but different
* object instance does nothing.
*
* @see {@link get} for retrieving scoped items from the pool
*
* @stability stable
* @category combinators
* @since 2.0.0
*/
export const invalidate: {
/**
* Invalidates the specified item so the pool can remove it and reallocate the
* item, lazily if needed.
*
* **When to use**
*
* Use to prevent a pooled item from being reused after it becomes unsuitable,
* such as a stale connection or a resource that failed a health check.
*
* **Gotchas**
*
* The item is matched with strict equality. Passing an equivalent but different
* object instance does nothing.
*
* @see {@link get} for retrieving scoped items from the pool
*
* @stability stable
* @category combinators
* @since 2.0.0
*/
(item: A): (self: Pool) => Effect.Effect
/**
* Invalidates the specified item so the pool can remove it and reallocate the
* item, lazily if needed.
*
* **When to use**
*
* Use to prevent a pooled item from being reused after it becomes unsuitable,
* such as a stale connection or a resource that failed a health check.
*
* **Gotchas**
*
* The item is matched with strict equality. Passing an equivalent but different
* object instance does nothing.
*
* @see {@link get} for retrieving scoped items from the pool
*
* @stability stable
* @category combinators
* @since 2.0.0
*/
(self: Pool, item: A): Effect.Effect
} = dual(2, (self: Pool, item: A): Effect.Effect =>
Effect.suspend(() => {
if (self.state.isShuttingDown) return Effect.void
for (const poolItem of self.state.items) {
if (poolItem.exit._tag === "Success" && poolItem.exit.value === item) {
poolItem.disableReclaim = true
return Effect.uninterruptible(invalidatePoolItem(self, poolItem))
}
}
return Effect.void
}))
/**
* Reserves a leased item for exclusive use until the scope closes. This
* removes the item's remaining capacity from the pool but does not wait
* for existing leases to finish. It has no effect when per-item concurrency is
* `1` or the pool does not contain the item.
*
* @see {@link get} for acquiring an item
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
export const reserve: {
/**
* Reserves a leased item for exclusive use until the scope closes. This
* removes the item's remaining capacity from the pool but does not wait
* for existing leases to finish. It has no effect when per-item concurrency is
* `1` or the pool does not contain the item.
*
* @see {@link get} for acquiring an item
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
(item: A): (self: Pool) => Effect.Effect
/**
* Reserves a leased item for exclusive use until the scope closes. This
* removes the item's remaining capacity from the pool but does not wait
* for existing leases to finish. It has no effect when per-item concurrency is
* `1` or the pool does not contain the item.
*
* @see {@link get} for acquiring an item
*
* @stability stable
* @category combinators
* @since 4.0.0
*/
(self: Pool, item: A): Effect.Effect
} = dual(
2,
(self: Pool, item: A): Effect.Effect =>
Effect.asVoid(Effect.acquireRelease(
Effect.sync(() => {
if (self.config.concurrency === 1) return undefined
for (const poolItem of self.state.items) {
if (poolItem.exit._tag !== "Success" || poolItem.exit.value !== item) continue
const existing = reservations.get(poolItem)
if (existing === undefined) self.state.usage += self.config.concurrency - 1
reservations.set(poolItem, (existing ?? 0) + 1)
removeAvailable(self, poolItem)
return poolItem
}
return undefined
}),
(poolItem) =>
core.withFiber((fiber) => {
if (poolItem === undefined) return internal.void
const remaining = (reservations.get(poolItem) ?? 1) - 1
if (remaining > 0) {
reservations.set(poolItem, remaining)
return internal.void
}
reservations.delete(poolItem)
self.state.usage -= self.config.concurrency - 1
if (
!self.state.isShuttingDown &&
self.state.items.has(poolItem) &&
!self.state.invalidated.has(poolItem) &&
poolItem.refCount < self.config.concurrency
) {
addAvailableFront(self, poolItem)
wakeWaiters(self, fiber, self.config.concurrency - poolItem.refCount)
}
return internal.void
})
))
)
const invalidatePoolItem = (self: Pool, poolItem: PoolItem): Effect.Effect =>
Effect.suspend(() => {
if (!self.state.items.has(poolItem)) {
return Effect.void
} else if (poolItem.refCount === 0) {
self.state.items.delete(poolItem)
removeAvailable(self, poolItem)
self.state.invalidated.delete(poolItem)
return Effect.asVoid(Effect.flatMap(
poolItem.finalizer,
() => Effect.forkIn(Effect.interruptible(resize(self)), self.state.scope, { startImmediately: true })
))
}
self.state.invalidated.add(poolItem)
removeAvailable(self, poolItem)
// An invalidated item stops counting towards the pool's active size, so the
// pool is now below target and has to top itself back up. Waiting for the
// last lease to be returned would strand anybody already queued: the item
// they are waiting for is never coming back.
return Effect.asVoid(
Effect.forkIn(Effect.interruptible(resize(self)), self.state.scope, { startImmediately: true })
)
})
const resize = (self: Pool): Effect.Effect =>
self.state.resizeSemaphore.withPermits(1)(resizeLoop(self))
const resizeLoop = (self: Pool): Effect.Effect =>
Effect.suspend(() => {
const active = activeSize(self)
const target = targetSize(self)
if (active >= target) {
return Effect.void
}
const toAcquire = target - active
const acquireOne = self.config.strategy === strategyNoop
? allocate(self)
: Effect.flatMap(
self.config.strategy.reclaim(self),
(item) => item ? Effect.succeed(item) : allocate(self)
)
if (toAcquire === 1) {
const acquired = Effect.tap(acquireOne, wakeAll(self))
return self.config.isFixed
? Effect.asVoid(acquired)
: Effect.flatMap(acquired, (item) => item.exit._tag === "Failure" ? Effect.void : resizeLoop(self))
}
const acquired = acquireOne.pipe(
Effect.replicateEffect(toAcquire, { concurrency: toAcquire }),
Effect.tap(wakeAll(self))
)
return self.config.isFixed
? Effect.asVoid(acquired)
: Effect.flatMap(
acquired,
(items) => items.some((_) => _.exit._tag === "Failure") ? Effect.void : resizeLoop(self)
)
})
const allocate = (self: Pool): Effect.Effect> =>
internal.uninterruptibleMask((restore) =>
core.withFiber((fiber) => {
const impl = self as PoolImpl
const scope = internal.scopeMakeUnsafe()
const previousContext = fiber.context
fiber.setContext(Context.add(impl[AcquireContext], Scope.Scope, scope))
const use = Effect.flatMap(Effect.exit(impl[Acquire]), (exit) => {
const item: PoolItem = {
exit,
finalizer: Effect.catchCause(Scope.close(scope, exit), reportUnhandledError),
refCount: 0,
idleSince: impl[PoolClock].currentTimeMillisUnsafe(),
disableReclaim: false,
isAvailable: false,
availablePrevious: undefined,
availableNext: undefined,
release: undefined as any
}
item.release = constant(releaseItem(self, item))
const onAcquire = Effect.suspend(() =>
// A borrower may have removed the item before the callback runs.
self.state.items.has(item) ? self.config.strategy.onAcquire(item) : Effect.void
)
if (exit._tag === "Success") {
self.state.items.add(item)
addAvailable(self, item)
return self.config.strategy === strategyNoop ? Effect.succeed(item) : Effect.as(onAcquire, item)
}
// Deliver a waiting borrower its own failure; otherwise retain the slot.
const waiter = firstHealthyAvailable(self) === undefined ? self.state.waiters.values().next().value : undefined
if (waiter !== undefined) {
waiter(exit)
} else {
self.state.items.add(item)
addAvailable(self, item)
}
// Do not let cleanup delay a replacement acquisition.
return Effect.as(
Effect.forkIn(Effect.andThen(item.finalizer, onAcquire), self.state.scope, { startImmediately: true }),
item
)
})
return internal.onExitPrimitive(
restore(use) as Effect.Effect>,
(exit) => {
fiber.setContext(previousContext)
return exit._tag === "Failure" ? internal.scopeCloseUnsafe(scope, exit) : undefined
},
true
)
})
)
const targetSize = (self: Pool) => {
if (self.state.isShuttingDown) return 0
if (self.config.isFixed) return self.config.minSize
const utilization = self.state.usage / self.config.targetUtilization
const target = Math.ceil(utilization / self.config.concurrency)
return Math.min(Math.max(self.config.minSize, target), self.config.maxSize)
}
const activeSize = (self: Pool) => {
return self.state.items.size - self.state.invalidated.size
}
// -----------------------------------------------------------------------------
// Strategy
// -----------------------------------------------------------------------------
const strategyNoop: Strategy = {
run: (_) => Effect.void,
onAcquire: (_) => Effect.void,
reclaim: (_) => Effect.undefined
}
const strategyCreationTTL = Effect.fnUntraced(function*(ttl: Duration.Input) {
const clock = yield* Clock
const queue = yield* Queue.unbounded>()
const ttlMillis = Duration.toMillis(Duration.fromInputUnsafe(ttl))
const creationTimes = new WeakMap, number>()
return identity>({
run: (pool) => {
const process = (item: PoolItem): Effect.Effect =>
Effect.suspend(() => {
if (!pool.state.items.has(item) || pool.state.invalidated.has(item)) {
return Effect.void
}
const now = clock.currentTimeMillisUnsafe()
const created = creationTimes.get(item)!
const remaining = ttlMillis - (now - created)
return remaining > 0
? Effect.delay(process(item), remaining)
: invalidatePoolItem(pool, item)
})
return Queue.take(queue).pipe(
Effect.tap(process),
Effect.forever({ disableYield: true })
)
},
onAcquire: (item) =>
Effect.suspend(() => {
creationTimes.set(item, clock.currentTimeMillisUnsafe())
return Queue.offer(queue, item)
}),
reclaim: (_) => Effect.undefined
})
})
const strategyUsageTTL = Effect.fnUntraced(function*(ttl: Duration.Input) {
const clock = yield* Clock
const ttlMillis = Duration.toMillis(Duration.fromInputUnsafe(ttl))
return identity>({
run: (pool) => {
const process: Effect.Effect = Effect.suspend(() => {
if (activeSize(pool) <= targetSize(pool)) return Effect.void
const expired = clock.currentTimeMillisUnsafe() - ttlMillis
let oldest: PoolItem | undefined
for (const item of pool.state.items) {
if (item.refCount > 0 || item.idleSince > expired) continue
if (pool.state.invalidated.has(item) || reservations.has(item)) continue
if (oldest === undefined || item.idleSince < oldest.idleSince) oldest = item
}
return oldest === undefined ? Effect.void : Effect.flatMap(invalidatePoolItem(pool, oldest), () => process)
})
return process.pipe(
Effect.delay(ttl),
Effect.forever({ disableYield: true })
)
},
onAcquire: (_) => Effect.void,
reclaim: (_) => Effect.undefined
})
})
const reportUnhandledError = (cause: Cause.Cause) =>
Effect.withFiber((fiber) => {
const unhandledLogLevel = fiber.getRef(UnhandledLogLevel)
if (unhandledLogLevel) {
return Effect.logWithLevel(unhandledLogLevel)(
"Unhandled error in pool finalizer",
cause
)
}
return Effect.void
})