diff --git a/core/src/main/scala/org/typelevel/keypool/KeyPool.scala b/core/src/main/scala/org/typelevel/keypool/KeyPool.scala index 40bcd697..16df9baf 100644 --- a/core/src/main/scala/org/typelevel/keypool/KeyPool.scala +++ b/core/src/main/scala/org/typelevel/keypool/KeyPool.scala @@ -32,12 +32,17 @@ import org.typelevel.keypool.internal._ * This pools internal guarantees are that the max number of values are in the pool at any time, not * maximum number of operations. To do the latter application level bounds should be used. * - * A background reaper thread is kept alive for the length of the key pools life. + * A background reaper thread is kept alive for the length of the key pool's life. * * When resources are taken from the pool they are received as a [[Managed]]. This [[Managed]] has a - * Ref to a [[Reusable]] which indicates whether or not the pool can reuse the resource. + * `Ref` to a [[Reusable]] which indicates whether the pool can reuse the resource. + * + * Unlike [[Pool]], which is a single-key convenience wrapper, `KeyPool` partitions resources by a + * user-provided key type `A` and enforces per-key limits and accounting. + * + * @see + * [[Pool]] */ - trait KeyPool[F[_], A, B] { /** @@ -346,38 +351,119 @@ object KeyPool { onReaperException ) + /** + * Register a callback `f` to run after a new [[Managed]] item is created. Any error raised by + * the callback is ignored, and the created item is returned unchanged. + * + * @note + * If multiple callbacks are registered, their execution order is not guaranteed and callers + * should not rely on any particular ordering. + */ def doOnCreate(f: B => F[Unit]): Builder[F, A, B] = - copy(kpRes = { (k: A) => this.kpRes(k).flatMap(v => Resource.eval(f(v).attempt.void.as(v))) }) - + copy(kpRes = { (k: A) => this.kpRes(k).flatMap(v => Resource.eval(f(v).voidError.as(v))) }) + + /** + * Register a callback `f` to run when a [[Managed]] item is about to be destroyed. Any error + * raised by the callback is ignored, and the item will be destroyed regardless. + * + * @note + * If multiple callbacks are registered, their execution order is not guaranteed and callers + * should not rely on any particular ordering. + */ def doOnDestroy(f: B => F[Unit]): Builder[F, A, B] = copy(kpRes = { (k: A) => - this.kpRes(k).flatMap(v => Resource.make(Applicative[F].unit)(_ => f(v).attempt.void).as(v)) + this.kpRes(k).flatMap(v => Resource.make(Applicative[F].unit)(_ => f(v).voidError).as(v)) }) + /** + * Set the default [[Reusable]] state applied when resources are returned to the pool. This + * default can be overridden per-resource via [[Managed.canBeReused]] from + * [[KeyPool.take(k:A)* KeyPool.take]]. If not configured, the default is [[Reusable.Reuse]]. + * + * @param defaultReuseState + * whether resources returned to the pool should be reused ([[Reusable.Reuse]]) or destroyed + * ([[Reusable.DontReuse]]) by default. + */ def withDefaultReuseState(defaultReuseState: Reusable): Builder[F, A, B] = copy(kpDefaultReuseState = defaultReuseState) + /** + * Set how long an idle resource is allowed to remain in the pool before it becomes eligible for + * eviction. If not configured, the builder defaults to + * [[KeyPool.Builder.Defaults.idleTimeAllowedInPool 30 seconds]]. + * + * @param duration + * maximum idle time allowed in the pool. + */ def withIdleTimeAllowedInPool(duration: Duration): Builder[F, A, B] = copy(idleTimeAllowedInPool = duration) + /** + * Set the interval between eviction runs of the pool reaper. If not configured, the builder + * defaults to [[KeyPool.Builder.Defaults.durationBetweenEvictionRuns 5 seconds]]. + * + * @param duration + * time between successive eviction runs. + */ def withDurationBetweenEvictionRuns(duration: Duration): Builder[F, A, B] = copy(durationBetweenEvictionRuns = duration) + /** + * Set the maximum number of idle resources allowed per key. If not configured, the builder + * defaults to [[KeyPool.Builder.Defaults.maxPerKey 100]]. + * + * @param f + * function returning max idle count for a given key. + */ def withMaxPerKey(f: A => Int): Builder[F, A, B] = copy(kpMaxPerKey = f) + /** + * Set the maximum number of idle resources allowed across all keys. If not configured, the + * builder defaults to [[KeyPool.Builder.Defaults.maxIdle 100]]. + * + * @param maxIdle + * maximum idle resources in the pool. + */ def withMaxIdle(maxIdle: Int): Builder[F, A, B] = copy(kpMaxIdle = maxIdle) + /** + * Set the maximum total number of concurrent resources permitted by the pool. If not + * configured, the builder defaults to [[KeyPool.Builder.Defaults.maxTotal 100]]. + * + * @param total + * maximum total resources. + */ def withMaxTotal(total: Int): Builder[F, A, B] = copy(kpMaxTotal = total) + /** + * Set the [[Fairness]] policy for acquiring permits from the global semaphore controlling total + * resources. If not configured, the builder defaults to + * [[KeyPool.Builder.Defaults.fairness Fairness.Fifo]]. + * + * @param fairness + * fairness policy - [[Fairness.Fifo]] or [[Fairness.Lifo]]. + */ def withFairness(fairness: Fairness): Builder[F, A, B] = copy(fairness = fairness) + /** + * Register a handler `f` invoked for any `Throwable` observed by the background reaper; it runs + * when the reaper loop reports an error. + * + * The provided function is invoked with any `Throwable` observed in the reaper loop. If the + * handler itself fails, that failure is swallowed and thereby ignored - the reaper will + * continue running and may invoke the handler again for subsequent errors. + */ def withOnReaperException(f: Throwable => F[Unit]): Builder[F, A, B] = copy(onReaperException = f) + /** + * Create a `KeyPool` wrapped in a `Resource`, initializing pool internals from the configured + * builder parameters and defaults. + */ def build: Resource[F, KeyPool[F, A, B]] = { def keepRunning[Z](fa: F[Z]): F[Z] = fa.onError { case e => onReaperException(e) }.attempt >> keepRunning(fa) @@ -409,6 +495,11 @@ object KeyPool { } object Builder { + + /** + * Create a new `Builder` for a `Pool` from a `Resource` that produces values stored in the + * pool. + */ def apply[F[_]: Temporal, A, B]( res: A => Resource[F, B] ): Builder[F, A, B] = new Builder[F, A, B]( @@ -423,6 +514,10 @@ object KeyPool { Defaults.onReaperException[F] ) + /** + * Convenience constructor that accepts `create` and `destroy` functions and builds a `Resource` + * internally. + */ def apply[F[_]: Temporal, A, B]( create: A => F[B], destroy: B => F[Unit] diff --git a/core/src/main/scala/org/typelevel/keypool/Managed.scala b/core/src/main/scala/org/typelevel/keypool/Managed.scala index ac3b62ce..00b4acd0 100644 --- a/core/src/main/scala/org/typelevel/keypool/Managed.scala +++ b/core/src/main/scala/org/typelevel/keypool/Managed.scala @@ -29,6 +29,22 @@ import cats.effect.kernel._ * * This knows whether it was reused or not, and has a reference that when it leaves the controlling * scope will dictate whether it is shutdown or returned to the pool. + * + * @param value + * the underlying resource held by this `Managed` instance. + * @param isReused + * indicates whether the resource was taken from the pool (`true`) or newly created (`false`). + * @param canBeReused + * A mutable reference controlling reuse: when the `Managed` is released this `Ref` determines + * whether the resource is returned to the pool or shut down. + * + * @note + * If the caller does not modify `canBeReused` on the returned `Managed`, the pool's default reuse + * state (configured via [[KeyPool.Builder.withDefaultReuseState]]) will be used when the resource + * is released. + * + * @see + * [[Reusable]] */ final class Managed[F[_], A] private[keypool] ( val value: A, diff --git a/core/src/main/scala/org/typelevel/keypool/Pool.scala b/core/src/main/scala/org/typelevel/keypool/Pool.scala index 1e1bffd3..546aedcf 100644 --- a/core/src/main/scala/org/typelevel/keypool/Pool.scala +++ b/core/src/main/scala/org/typelevel/keypool/Pool.scala @@ -30,12 +30,17 @@ import scala.concurrent.duration._ * This pools internal guarantees are that the max number of values are in the pool at any time, not * maximum number of operations. To do the latter application level bounds should be used. * - * A background reaper thread is kept alive for the length of the pools life. + * A background reaper thread is kept alive for the length of the pool's life. * * When resources are taken from the pool they are received as a [[Managed]]. This [[Managed]] has a - * Ref to a [[Reusable]] which indicates whether or not the pool can reuse the resource. + * `Ref` to a [[Reusable]] which indicates whether the pool can reuse the resource. + * + * `Pool` is a convenience, single-key specialization of [[KeyPool]] that does not partition + * resources by key and exposes simpler `take`/`state` APIs. + * + * @see + * [[KeyPool]] */ - trait Pool[F[_], B] { /** @@ -100,32 +105,100 @@ object Pool { onReaperException ) + /** + * Register a callback `f` to run after a new [[Managed]] item is created. Any error raised by + * the callback is ignored, and the created item is returned unchanged. + * + * @note + * If multiple callbacks are registered, their execution order is not guaranteed and callers + * should not rely on any particular ordering. + */ def doOnCreate(f: B => F[Unit]): Builder[F, B] = copy(kpRes = this.kpRes.flatMap(v => Resource.eval(f(v).attempt.void.as(v)))) + /** + * Register a callback `f` to run when a [[Managed]] item is about to be destroyed. Any error + * raised by the callback is ignored, and the item will be destroyed regardless. + * + * @note + * If multiple callbacks are registered, their execution order is not guaranteed and callers + * should not rely on any particular ordering. + */ def doOnDestroy(f: B => F[Unit]): Builder[F, B] = copy(kpRes = this.kpRes.flatMap(v => Resource.make(Applicative[F].unit)(_ => f(v).attempt.void).as(v)) ) + /** + * Set the default [[Reusable]] state applied when resources are returned to the pool. This + * default can be overridden per-resource via [[Managed.canBeReused]] from [[Pool.take]]. If not + * configured, the default is [[Reusable.Reuse]]. + * + * @param defaultReuseState + * whether resources returned to the pool should be reused ([[Reusable.Reuse]]) or destroyed + * ([[Reusable.DontReuse]]) by default. + */ def withDefaultReuseState(defaultReuseState: Reusable): Builder[F, B] = copy(kpDefaultReuseState = defaultReuseState) + /** + * Set how long an idle resource is allowed to remain in the pool before it becomes eligible for + * eviction. If not configured, the builder defaults to 30 seconds. + * + * @param duration + * maximum idle time allowed in the pool. + */ def withIdleTimeAllowedInPool(duration: Duration): Builder[F, B] = copy(idleTimeAllowedInPool = duration) + /** + * Set the interval between eviction runs of the pool reaper. If not configured, the builder + * defaults to 5 seconds. + * + * @param duration + * time between successive eviction runs. + */ def withDurationBetweenEvictionRuns(duration: Duration): Builder[F, B] = copy(durationBetweenEvictionRuns = duration) + /** + * Set the maximum number of idle resources allowed across the pool. If not configured, the + * builder defaults to 100. + * + * @param maxIdle + * maximum idle resources in the pool. + */ def withMaxIdle(maxIdle: Int): Builder[F, B] = copy(kpMaxIdle = maxIdle) + /** + * Set the maximum total number of concurrent resources permitted by the pool. If not + * configured, the builder defaults to 100. + * + * @param total + * maximum total resources. + */ def withMaxTotal(total: Int): Builder[F, B] = copy(kpMaxTotal = total) + /** + * Set the [[Fairness]] policy for acquiring permits from the global semaphore controlling total + * resources. If not configured, the builder defaults to [[Fairness.Fifo]]. + * + * @param fairness + * fairness policy - [[Fairness.Fifo]] or [[Fairness.Lifo]]. + */ def withFairness(fairness: Fairness): Builder[F, B] = copy(fairness = fairness) + /** + * Register a handler `f` invoked for any `Throwable` observed by the background reaper; it runs + * when the reaper loop reports an error. + * + * The provided function is invoked with any `Throwable` observed in the reaper loop. If the + * handler itself fails, that failure is swallowed and thereby ignored - the reaper will + * continue running and may invoke the handler again for subsequent errors. + */ def withOnReaperException(f: Throwable => F[Unit]): Builder[F, B] = copy(onReaperException = f) @@ -142,6 +215,14 @@ object Pool { onReaperException = onReaperException ) + /** + * Create a `Pool` wrapped in a `Resource`, initializing pool internals from the configured + * builder parameters and defaults. + * + * @note + * The implementation is a thin single-key specialization that delegates to + * [[KeyPool.Builder]] under the hood. + */ def build: Resource[F, Pool[F, B]] = { toKeyPoolBuilder.build.map { inner => new Pool[F, B] { @@ -153,6 +234,11 @@ object Pool { } object Builder { + + /** + * Create a new `Builder` for a `Pool` from a `Resource` that produces values stored in the + * pool. + */ def apply[F[_]: Temporal, B]( res: Resource[F, B] ): Builder[F, B] = new Builder[F, B]( @@ -166,6 +252,10 @@ object Pool { Defaults.onReaperException[F] ) + /** + * Convenience constructor that accepts `create` and `destroy` functions and builds a `Resource` + * internally. + */ def apply[F[_]: Temporal, B]( create: F[B], destroy: B => F[Unit] diff --git a/core/src/main/scala/org/typelevel/keypool/Reusable.scala b/core/src/main/scala/org/typelevel/keypool/Reusable.scala index 9da8d07f..7956c976 100644 --- a/core/src/main/scala/org/typelevel/keypool/Reusable.scala +++ b/core/src/main/scala/org/typelevel/keypool/Reusable.scala @@ -22,10 +22,10 @@ package org.typelevel.keypool /** - * Reusable is a Coproduct of the two states a Resource can be in at the end of its lifetime. + * Reusable is a Coproduct of the two states a `Resource` can be in at the end of its lifetime. * - * If it is Reuse then it will be attempted to place back in the pool, if it is in DontReuse the - * resource will be shutdown. + * If it is `Reusable.Reuse` then it will be attempted to place back in the pool, if it is in + * `Reusable.DontReuse` the resource will be shutdown. */ sealed trait Reusable object Reusable { diff --git a/docs/classes/directory.conf b/docs/classes/directory.conf new file mode 100644 index 00000000..5042b325 --- /dev/null +++ b/docs/classes/directory.conf @@ -0,0 +1,5 @@ +laika.navigationOrder = [ + pool.md + keypool.md + misc.md +] diff --git a/docs/classes/examples/directory.conf b/docs/classes/examples/directory.conf new file mode 100644 index 00000000..a1b2cc65 --- /dev/null +++ b/docs/classes/examples/directory.conf @@ -0,0 +1 @@ +laika.targetFormats = [] diff --git a/docs/classes/examples/keypool-example.md b/docs/classes/examples/keypool-example.md new file mode 100644 index 00000000..b057f441 --- /dev/null +++ b/docs/classes/examples/keypool-example.md @@ -0,0 +1,54 @@ +```scala 3 +import cats.data.NonEmptyVector +import cats.effect.* +import cats.syntax.all.* +import com.comcast.ip4s.* +import fs2.io.net.* +import fs2.Stream +import org.typelevel.keypool.* +import scala.concurrent.duration.* + +// Runs the program on the given cluster. +def program[F[_]: {Temporal, Network}]( + cluster: NonEmptyVector[SocketAddress[Host]] +): F[Unit] = + KeyPool + // Creates a keyed pool that manages TCP connections to cluster nodes. + .Builder: (key: Int) => + // Creates a socket connection to one of the cluster nodes. + // Assumes that `0 < key && key < cluster.length` (see below). + Network[F].client(cluster.getUnsafe(key)) + .withMaxTotal(20) // configures `maxTotal` + .withMaxIdle(15) // configures `maxIdle` + .withMaxPerKey(Function.const(10)) // configures `maxPerKey` + .withIdleTimeAllowedInPool(5.seconds) // configures `idleTimeAllowedInPool`. + .build + .use: pool => + eventProducer + // Processes events in parallel using pooled connections. + .parEvalMapUnordered(10): req => + // Computes the key so that each request is dispatched to a specific + // cluster node based on up to the first 3 characters of the request. + val key = req.take(3).hash % cluster.length + // Takes a connection from the pool. + pool + .take(key) + .use: managed => + // Uses the connection to send a query to the server. + serverQuery(managed.value, req).flatMap: res => + // Marks the connection as non‑reusable if the response is incorrect. + managed.canBeReused + .set(Reusable.DontReuse) + .unlessA(isCorrect(res)) + .compile + .drain + +// Produces a stream of events. +def eventProducer[F[_]]: Stream[F, String] = ??? + +// Sends a request over the given socket connection and returns the server’s response. +def serverQuery[F[_]](socket: Socket[F], req: String): F[String] = ??? + +// Validates the response. +def isCorrect(res: String): Boolean = ??? +``` diff --git a/docs/classes/examples/pool-example.md b/docs/classes/examples/pool-example.md new file mode 100644 index 00000000..8b54e182 --- /dev/null +++ b/docs/classes/examples/pool-example.md @@ -0,0 +1,42 @@ +```scala 3 +import cats.effect.* +import cats.syntax.all.* +import com.comcast.ip4s.* +import fs2.io.net.* +import fs2.Stream +import org.typelevel.keypool.* +import scala.concurrent.duration.* + +// Runs the program on the given address. +def program[F[_]: {Temporal, Network}](address: SocketAddress[Host]): F[Unit] = + Pool + // Creates a pool that manages TCP connections to the given address. + .Builder(Network[F].client(address)) + .withMaxTotal(20) // configures `maxTotal` + .withMaxIdle(15) // configures `maxIdle` + .withIdleTimeAllowedInPool(5.seconds) // configures `idleTimeAllowedInPool` + .build + .use: pool => + eventProducer + // Processes events in parallel using pooled connections. + .parEvalMapUnordered(10): req => + // Takes a connection from the pool. + pool.take.use: managed => + // Uses the connection to make a query to the server. + serverQuery(managed.value, req).flatMap: res => + // Marks the connection as non‑reusable if the response is incorrect. + managed.canBeReused + .set(Reusable.DontReuse) + .unlessA(isCorrect(res)) + .compile + .drain + +// Produces a stream of events. +def eventProducer[F[_]]: Stream[F, String] = ??? + +// Sends a request over the given socket connection and returns the server’s response. +def serverQuery[F[_]](socket: Socket[F], req: String): F[String] = ??? + +// Validates the response. +def isCorrect(res: String): Boolean = ??? +``` diff --git a/docs/classes/keypool.md b/docs/classes/keypool.md new file mode 100644 index 00000000..c41192b3 --- /dev/null +++ b/docs/classes/keypool.md @@ -0,0 +1,49 @@ +# KeyPool + +`KeyPool` manages reusable resources partitioned by a user-provided key of arbitrary type. It allows +to enforce both per-key and global limits on idle and total resources. It hands out resources as +[`Managed`] values which include a per-resource [`Reusable`] flag so callers can indicate whether +the value should be returned to the pool or destroyed. A background reaper evicts idle resources +after a configurable timeout. `KeyPool` instances are constructed with [KeyPool.Builder] that allows +to configure creation/destruction callbacks, reuse policy, eviction timing, limits, +and [fairness][Fairness]. + +See also [`Pool`] – a convenience, single-key specialization of `KeyPool` that does not +partition resources by key and exposes simpler APIs. + +## KeyPool.Builder + +`KeyPool.Builder` constructs a configured `KeyPool` instance. It takes the managed resource +factory; it also allows to tune pool policies and limits. + +The table below enumerates all the available configuration parameters. + +| Parameter Name | Parameter Type | Default Value | Description | +|-------------------------------|----------------|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `defaultReuseState` | [`Reusable`] | `Reusable.Reuse` | Default [`Reusable`] state applied when resources are returned to the pool; [`Managed.canBeReused`][`Managed`] can override it per acquisition. | +| `durationBetweenEvictionRuns` | `Duration` | 5 seconds | Interval between successive reaper eviction runs; a negative or infinite value disables the reaper. **Note**: value **0** would make the reaper run nonstop, which can be inefficient and CPU-consuming. | +| `fairness` | [`Fairness`] | `Fairness.Fifo` | Fairness policy for acquiring permits from the global semaphore. | +| `idleTimeAllowedInPool` | `Duration` | 30 seconds | How long an idle resource is allowed to remain in the pool before the reaper considers it for eviction. | +| `maxIdle` | `Int` | 100 | Cap on the idle resources tracked across all keys. | +| `maxPerKey` | `A => Int` | 100 | Maximum number of idle resources kept per key `A`. **Note**: it only limits idle resources per key; it does not limit how many resources can be checked out concurrently. | +| `maxTotal` | `Int` | 100 | Global limit on the total number of concurrent resources the pool will hold. | + +The builder also allows to configure the following lifecycle callbacks. + +| Callback Registrar | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------| +| `doOnCreate` | Register a callback that runs after a new item is created; callback failures are ignored. | +| `doOnDestroy` | Register a callback that runs when an item is about to be destroyed; callback failures are ignored. | +| `withOnReaperException` | Register a callback that is invoked when the background reaper observes a `Throwable`; callback failures are ignored. | + +## Example + +This example demonstrates how to use `KeyPool` to manage reusable TCP socket connections to a +cluster of nodes. It constructs a configured `KeyPool` whose keys determine which node each +connection targets, and runs a `Stream` that processes events in parallel using keyed, pooled +connections. + +@:include(examples/keypool-example.md) + +The implementations of `eventProducer`, `serverQuery`, and `isCorrect` are intentionally omitted +because they are not relevant to the example. diff --git a/docs/classes/misc.md b/docs/classes/misc.md new file mode 100644 index 00000000..efdbab14 --- /dev/null +++ b/docs/classes/misc.md @@ -0,0 +1,47 @@ +# Miscellaneous + +This page describes small core types used across the **keypool** implementation. + +## Fairness + +Represents a policy that controls the ordering of pending requests. It's defined as a coproduct type +with two cases: + +- `Fairness.Fifo` – first-in, first-out ordering. Pending requests are served in arrival order. This + avoids starvation and provides predictable, fair ordering across different callers or tenants. + Prefer this option when fairness, predictability, and avoiding starvation across clients is + important. + + **Example**: a public-facing service where you want older requests to be satisfied before newer + ones, so no client can be starved by others. + +- `Fairness.Lifo` – last-in, first-out ordering. The most recently queued request is served first. + This can be beneficial in bursty workloads where recently released resources are still "warm". + Prefer this option when you expect a lot of short-lived churn and want to favor recently-returned + resources for better locality and potential latency improvements. + + **Example**: HTTP keep-alive (persistent TCP connections) – reusing the most recently returned + connection avoids the latency of establishing a new TCP/TLS session and is less likely to fail + because of idle timeouts. + +Use [`Pool.Builder`] or [`KeyPool.Builder`] to set the policy when creating a pool instance. The +default is `Fairness.Fifo`. + +## Managed + +`Managed` represents an acquired resource together with reuse-tracking and a reference that +determines whether it is returned to the pool. `Managed` consists of the following fields: + +| Field Name | Field Type | Description | +|---------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `value` | `A` | The underlying resource of type `A` held by this `Managed` | +| `isReused` | `Boolean` | Indicates whether the resource was taken from the pool (`true`) or newly created (`false`). | +| `canBeReused` | `Ref[F, Reusable]` | A mutable reference controlling reuse: when the `Managed` is released this `Ref` determines whether the resource is returned to the pool or shut down. | + +### Reusable + +`Reusable` indicates whether a resource should be reused or shut down. It's defined as a coproduct +type with two cases: + +- `Reusable.Reuse` – the returned resource is kept in the pool and may be reused. +- `Reusable.DontReuse` – the returned resource must not be reused and should be shut down. diff --git a/docs/classes/pool.md b/docs/classes/pool.md new file mode 100644 index 00000000..3c7bb5c2 --- /dev/null +++ b/docs/classes/pool.md @@ -0,0 +1,50 @@ +# Pool + +`Pool` is a simple specialization of [`KeyPool`] that manages reusable resources without +partitioning by key. It enforces global limits on idle and total resources and hands out resources +as [`Managed`] values which include a per-resource [`Reusable`] flag so callers can indicate whether +the value should be returned to the pool or destroyed. A background reaper evicts idle resources +after a configurable timeout. `Pool` instances are constructed with [`Pool.Builder`] which allows +configuration of creation/destruction callbacks, reuse policy, eviction timing, limits, +and [fairness][Fairness]. + +See also [`KeyPool`] – a full-featured multi-key generalization that partitions resources by a user +provided key type and exposes per-key limits and accounting. + +## Pool.Builder + +`Pool.Builder` constructs a configured `Pool` instance. It takes the managed resource +factory; it also allows to tune pool policies and limits. + +The table below enumerates all the available configuration parameters. + +| Parameter Name | Parameter Type | Default Value | Description | +|-------------------------------|----------------|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `defaultReuseState` | [`Reusable`] | `Reusable.Reuse` | Default [`Reusable`] state applied when resources are returned to the pool; [`Managed.canBeReused`][`Managed`] can override it per acquisition. | +| `durationBetweenEvictionRuns` | `Duration` | 5 seconds | Interval between successive reaper eviction runs; a negative or infinite value disables the reaper. **Note**: value **0** would make the reaper run nonstop, which can be inefficient and CPU-consuming. | +| `fairness` | [`Fairness`] | `Fairness.Fifo` | Fairness policy for acquiring permits from the global semaphore. | +| `idleTimeAllowedInPool` | `Duration` | 30 seconds | How long an idle resource is allowed to remain in the pool before the reaper considers it for eviction. | +| `maxIdle` | `Int` | 100 | Cap on the idle resources tracked across all keys. | +| `maxTotal` | `Int` | 100 | Global limit on the total number of concurrent resources the pool will hold. | + +The builder also allows to configure the following lifecycle callbacks. + +| Callback Registrar | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------| +| `doOnCreate` | Register a callback that runs after a new item is created; callback failures are ignored. | +| `doOnDestroy` | Register a callback that runs when an item is about to be destroyed; callback failures are ignored. | +| `withOnReaperException` | Register a callback that is invoked when the background reaper observes a `Throwable`; callback failures are ignored. | + +**Note**: this builder delegates to [`KeyPool.Builder`] with key type `Unit` and uses `maxTotal` for +the `maxPerKey` parameter. + +## Example + +This example demonstrates how to use `Pool` to manage reusable TCP socket connections to a single +server node. It constructs a configured `Pool` from a socket `Resource` and runs a `Stream` that +processes events in parallel using pooled connections. + +@:include(examples/pool-example.md) + +The implementations of `eventProducer`, `serverQuery`, and `isCorrect` are intentionally omitted +because they are not relevant to the example. diff --git a/docs/directory.conf b/docs/directory.conf new file mode 100644 index 00000000..0521b525 --- /dev/null +++ b/docs/directory.conf @@ -0,0 +1,4 @@ +laika.navigationOrder = [ + index.md + classes +] diff --git a/docs/index.md b/docs/index.md index 0c9f1d14..e6e801e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,13 +2,41 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.typelevel/keypool_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.typelevel/keypool_2.13) +## Overview + +**keypool** provides a lightweight keyed resource pool abstraction for controlling concurrent access +to reusable resources. It limits concurrent acquisitions and reclaims resources to prevent leaks. +Per-key isolation and deterministic lifecycle semantics enable predictable behavior under load. + +The library is designed for integration within the [Typelevel][typelevel] ecosystem. It is +implemented on top of [Cats Effect][cats-effect] type classes using **Polymorphic Effects** (a.k.a. +**Tagless Final**). + +The library provides the following primary implementations: + +- [`Pool`] – a simple specialization that manages reusable resources without partitioning by + key. Use it when you need a single shared pool of identical resources, such as a pool of + connections to a single endpoint when connection partitioning isn't required. +- [`KeyPool`] – a general-purpose pool that partitions resources by a user-defined key. Use + it when you need per-key isolation and limits, for example: + - In cluster-based deployments, to prevent one node from starving others by partitioning + connections into separate per-node pools. + - In multi-tenant systems, to limit how many database connections or active API sessions each + tenant can use. + ## Quick Start -To use keypool in an existing SBT project with Scala 2.12 or a later version, add the following dependencies to your -`build.sbt` depending on your needs: +To use **keypool** in an existing SBT project with Scala 2.12 or a later version, add the following +dependencies to your `build.sbt` depending on your needs: ```scala -libraryDependencies ++= Seq( - "org.typelevel" %% "keypool" % "@VERSION@" -) +libraryDependencies += "org.typelevel" %% "keypool" % "@VERSION@" ``` + +**keypool** is cross-built with Scala versions 2.12, 2.13 and 3.3 for **JVM**, **Scala.js** and +**Scala Native**. + + +[cats-effect]: https://typelevel.org/cats-effect/ + +[typelevel]: https://typelevel.org