puddle

Types

Errors that can occur when applying a function to a pooled resource.

  • NoResourcesAvailable: Pool exhausted and no lazy capacity (non-blocking checkout).
  • CheckoutTimeout: Timeout while waiting for a resource (blocking checkout).
  • PoolShuttingDown: Pool is shutting down, no new checkouts allowed.
pub type ApplyError {
  NoResourcesAvailable
  CheckoutTimeout
  PoolShuttingDown
}

Constructors

  • NoResourcesAvailable
  • CheckoutTimeout
  • PoolShuttingDown

Opaque builder type for configuring and creating a resource pool.

Use puddle.new/1 to create a builder, then chain configuration functions before calling puddle.start/2 or puddle.supervised/2.

pub opaque type Builder(resource_type, result_type)

Strategy for selecting which idle resource to check out.

  • FIFO (default): Oldest idle resource first. Provides fair ordering.
  • LIFO: Most recently returned resource first. Better cache locality.
pub type CheckoutStrategy {
  FIFO
  LIFO
}

Constructors

  • FIFO
  • LIFO

Strategy for when resources are created in the pool.

  • Eager (default): Create all resources at pool startup.
  • Lazy: Create resources on first demand, up to the pool size.
pub type CreationStrategy {
  Lazy
  Eager
}

Constructors

  • Lazy
  • Eager

Internal message type for the pool manager actor.

This type is opaque - use the public API functions instead of sending these messages directly.

pub opaque type ManagerMessage(resource_type, result_type)

The result of a resource usage function, indicating whether to keep or discard the resource after use.

  • Keep(value): Return the resource to the pool with the result value.
  • Discard(value): Destroy the resource and create a replacement, returning the value.
pub type Next(result_type) {
  Keep(result_type)
  Discard(result_type)
}

Constructors

  • Keep(result_type)
  • Discard(result_type)

Current state of the resource pool.

  • Ready: Idle resources are available for immediate checkout.
  • Full: All resources are busy, no waiters queued.
  • Overloaded: All resources are busy, requests are queued.
pub type PoolState {
  Ready
  Full
  Overloaded
}

Constructors

  • Ready
  • Full
  • Overloaded

Status snapshot of the resource pool.

Contains the current state, configured size, and counts of available, busy, and waiting resources/requests.

pub type PoolStatus {
  PoolStatus(
    state: PoolState,
    size: Int,
    available: Int,
    busy: Int,
    waiting: Int,
  )
}

Constructors

  • PoolStatus(
      state: PoolState,
      size: Int,
      available: Int,
      busy: Int,
      waiting: Int,
    )

Internal message type for resource worker actors.

This type is opaque - use the public API functions instead.

pub opaque type ResourceMessage(resource_type, result_type)

Values

pub fn apply(
  manager: process.Subject(
    ManagerMessage(resource_type, result_type),
  ),
  fun: fn(resource_type) -> Next(result_type),
  timeout: Int,
  rest: fn(Result(result_type, ApplyError)) -> Result(
    a,
    ApplyError,
  ),
) -> Result(a, ApplyError)

Apply a function to a pooled resource (non-blocking checkout).

Checks out a resource, applies fun to it, and returns the result. If no resource is available and the pool has no lazy capacity, returns Error(NoResourcesAvailable) immediately.

The fun function receives the resource and must return a Next(result_type):

  • puddle.keep(value) - return resource to pool, continue with value
  • puddle.discard(value) - destroy resource, create replacement, continue with value

The timeout is the maximum time (in milliseconds) to wait for:

  • Resource checkout (if pool not exhausted)
  • Function execution and result

Uses the rest callback to handle the final result or error.

let result = {
  use r <- puddle.apply(manager, fn(conn) {
    case db.query(conn, "SELECT 1") {
      Ok(rows) -> puddle.keep(rows)
      Error(_) -> puddle.discard([])
    }
  }, 1000)
  r
}
pub fn apply_blocking(
  manager: process.Subject(
    ManagerMessage(resource_type, result_type),
  ),
  fun: fn(resource_type) -> Next(result_type),
  timeout: Int,
  rest: fn(Result(result_type, ApplyError)) -> Result(
    a,
    ApplyError,
  ),
) -> Result(a, ApplyError)

Apply a function to a pooled resource (blocking checkout).

Similar to apply/4, but if all resources are busy, the request is queued until a resource becomes available or the timeout expires.

If a resource becomes available within timeout milliseconds, the function is applied and the result returned. Otherwise, returns Error(CheckoutTimeout).

The timeout applies to the total time waiting for a resource plus function execution.

let result = {
  use r <- puddle.apply_blocking(manager, fn(n) { puddle.keep(n) }, 5000)
  r
}
pub fn checkout_strategy(
  builder: Builder(resource_type, result_type),
  strategy: CheckoutStrategy,
) -> Builder(resource_type, result_type)

Set the checkout strategy for selecting idle resources.

Default: FIFO

  • FIFO: Oldest idle resource first (fair ordering)
  • LIFO: Most recently returned resource first (better cache locality)
pub fn creation_strategy(
  builder: Builder(resource_type, result_type),
  strategy: CreationStrategy,
) -> Builder(resource_type, result_type)

Set the resource creation strategy.

Default: Eager

  • Eager: Create all resources at pool startup
  • Lazy: Create resources on first demand, up to size
pub fn discard(value: result_type) -> Next(result_type)

Signal to discard the resource and create a replacement.

The resource will be shut down via the on_shutdown callback and a new resource will be created (up to the pool size limit).

Returns Next(result_type) wrapping the value to pass to the continuation.

pub fn keep(value: result_type) -> Next(result_type)

Signal to keep the resource in the pool after use.

Returns Next(result_type) wrapping the value to pass to the continuation.

pub fn name(
  builder: Builder(resource_type, result_type),
  pool_name: process.Name(
    ManagerMessage(resource_type, result_type),
  ),
) -> Builder(resource_type, result_type)

Register the pool under a globally accessible name.

The pool can then be accessed from anywhere using process.named_subject(name) without passing the manager reference.

import gleam/erlang/process

let pool_name = process.new_name("my_db_pool")
let assert Ok(manager) =
  puddle.new(create_connection)
  |> puddle.size(10)
  |> puddle.name(pool_name)
  |> puddle.start(5000)

// Later, from anywhere:
let manager = process.named_subject(pool_name)
pub fn new(
  create_resource: fn() -> Result(resource_type, Nil),
) -> Builder(resource_type, result_type)

Create a new pool builder with a resource creation function.

The create_resource function is called to create new resources. It should return Ok(resource) on success or Error(Nil) on failure.

Default configuration:

  • Size: 10
  • Checkout strategy: FIFO
  • Creation strategy: Eager
  • Shutdown callback: no-op
  • Name: None
pub fn on_shutdown(
  builder: Builder(resource_type, result_type),
  callback: fn(resource_type) -> Nil,
) -> Builder(resource_type, result_type)

Set a callback to run when each resource is shut down.

The callback is called for every resource in the pool when:

  • The pool is shut down via puddle.shutdown/1
  • A resource is discarded via puddle.discard/1
  • A worker process crashes and is replaced

Use this to clean up resources (e.g., close database connections).

Default: no-op

pub fn shutdown(
  manager: process.Subject(
    ManagerMessage(resource_type, result_type),
  ),
) -> Nil

Gracefully shut down the resource pool.

Sends a shutdown signal to the pool manager. The manager will:

  1. Stop accepting new checkouts
  2. Wait for currently checked-out resources to be returned
  3. Call the on_shutdown callback for each resource
  4. Stop the manager actor

This function returns immediately; shutdown happens asynchronously. Use puddle.status/2 to monitor shutdown progress if needed.

puddle.shutdown(manager)
pub fn size(
  builder: Builder(resource_type, result_type),
  size: Int,
) -> Builder(resource_type, result_type)

Set the maximum number of resources in the pool.

Default: 10

When using Lazy creation strategy, resources are created up to this limit on demand. When using Eager strategy, this many resources are created at startup.

pub fn start(
  builder: Builder(resource_type, result_type),
  timeout: Int,
) -> Result(
  process.Subject(ManagerMessage(resource_type, result_type)),
  actor.StartError,
)

Start the resource pool and return a manager subject.

The pool is started as a supervised actor. The timeout parameter is the maximum time (in milliseconds) to wait for the pool to start and for initial resource creation (if using Eager strategy).

Returns Ok(manager_subject) on success, or Error(StartError) if the actor fails to start or resource creation fails.

let assert Ok(manager) =
  puddle.new(create_connection)
  |> puddle.size(10)
  |> puddle.start(5000)
pub fn status(
  manager: process.Subject(
    ManagerMessage(resource_type, result_type),
  ),
  timeout: Int,
) -> PoolStatus

Get the current status of the resource pool.

Returns a PoolStatus record containing:

  • state: Ready, Full, or Overloaded
  • size: configured pool size
  • available: number of idle resources
  • busy: number of checked-out resources
  • waiting: number of queued blocking requests

The timeout is the maximum time (in milliseconds) to wait for the status response from the manager.

let status = puddle.status(manager, 1000)
case status.state {
  puddle.Ready -> io.debug("Pool ready")
  puddle.Full -> io.debug("Pool full")
  puddle.Overloaded -> io.debug("Pool overloaded")
}
pub fn supervised(
  builder: Builder(resource_type, result_type),
  timeout: Int,
) -> supervision.ChildSpecification(
  process.Subject(ManagerMessage(resource_type, result_type)),
)

Create a child specification for running the pool under an OTP supervisor.

The pool will be started as a worker under the supervisor. The timeout parameter is the maximum time (in milliseconds) to wait for the pool to start and for initial resource creation (if using Eager strategy).

Use with gleam/otp/static_supervisor or gleam/otp/dynamic_supervisor.

import gleam/otp/static_supervisor

let child_spec =
  puddle.new(create_resource)
  |> puddle.size(5)
  |> puddle.supervised(5000)

let assert Ok(_supervisor) =
  static_supervisor.new(static_supervisor.OneForOne)
  |> static_supervisor.add(child_spec)
  |> static_supervisor.start
✨ Search Document