Skip to content

jamesgober/pool-mod

Repository files navigation

Rust logo
pool-mod
GENERIC OBJECT AND CONNECTION POOLING

crates.io Crates.io Downloads docs.rs CI MSRV

Async-safe min/max sizing, idle timeouts, max-lifetime enforcement, validation-on-borrow, health-check callbacks. Runtime-agnostic.


pool-mod is a generic pool for any resource that is expensive to build — database connections, HTTP clients, worker handles, parsers, large buffers. You describe the resource's lifecycle once by implementing a single trait; the pool takes care of sizing, blocking acquisition with timeouts, validation-on-borrow, and idle / lifetime expiry.

The pool is runtime-agnostic: it pulls in zero dependencies and carries no async runtime. The borrow guard returned on checkout is Send, so it works in synchronous code directly and in async code by acquiring on a blocking-friendly executor thread (such as tokio::task::spawn_blocking).



Features

  • Generic over any resource — pool connections, clients, threads, buffers, or anything else through one Manager trait.
  • Min / max sizingmin_idle resources are created up front and kept ready; the pool grows on demand up to max_size and never beyond it.
  • Blocking acquisition with timeoutsget waits up to a configured create_timeout; get_timeout overrides it per call, and try_get never blocks.
  • Validation-on-borrow — an optional validate hook (a health-check callback) runs on checkout; a resource that fails is discarded and replaced transparently.
  • Idle & max-lifetime expiry — stale resources are dropped and replaced, bounded by idle_timeout and max_lifetime. Applied lazily on checkout by default, or eagerly by an opt-in background reaper (reap_interval).
  • RAII return — the Pooled guard recycles and returns its resource automatically on drop. There is no release to forget and no way to leak a resource.
  • Thread-safe and cheap to sharePool is Send + Sync and clones into another handle onto the same pool.
  • Runtime-agnostic, zero-dependency — no async runtime, no third-party crates.
  • no_std-aware — the crate root compiles without std; the pool itself is behind the default std feature.



Installation

[dependencies]
pool-mod = "1.0"

MSRV is Rust 1.75. The crate is edition 2021 and builds on Linux, macOS, and Windows.


Quick Start

Implement Manager for your resource, then build a pool and borrow from it:

use pool_mod::{Manager, Pool};
use std::convert::Infallible;

// Describe how to create, reset, and (optionally) validate the resource.
struct Buffers {
    capacity: usize,
}

impl Manager for Buffers {
    type Resource = Vec<u8>;
    type Error = Infallible;

    fn create(&self) -> Result<Vec<u8>, Infallible> {
        Ok(Vec::with_capacity(self.capacity))
    }

    fn recycle(&self, buf: &mut Vec<u8>) -> Result<(), Infallible> {
        buf.clear(); // reuse the allocation, drop the contents
        Ok(())
    }
}

let pool = Pool::builder(Buffers { capacity: 4096 })
    .max_size(16)
    .min_idle(4)
    .build()
    .expect("configuration is valid");

// Borrow a buffer; it returns to the pool when `buf` is dropped.
let mut buf = pool.get().expect("a buffer is available");
buf.extend_from_slice(b"payload");
assert_eq!(buf.len(), 7);

How It Works

A pool owns up to max_size resources. Each checkout:

  1. Reuses an idle resource if one is available — after applying max_lifetime, idle_timeout, and the validate health check. A resource that is too old, too stale, or invalid is dropped and the pool moves on.
  2. Creates a new resource if none is idle and the pool has not reached max_size.
  3. Waits if the pool is saturated, until a resource is returned or the timeout elapses.

When a Pooled guard is dropped, its resource is passed to recycle and returned to the idle set. If recycling fails — or the pool has been closed — the resource is dropped instead and its slot is freed for a replacement.

Resource construction, validation, and recycling all run without the pool's internal lock held, so a slow create (opening a socket, say) never blocks other threads from returning resources. The lock guards only a small queue and a couple of counters.


Configuration

Configure through the Builder, or build a PoolConfig directly (for example, from a settings file).

Setting Default Meaning
max_size 10 Upper bound on resources owned at once (idle + checked out).
min_idle 0 Resources created up front and kept ready. Must be ≤ max_size.
create_timeout 30s How long get waits when saturated. None waits indefinitely.
idle_timeout None Replace a resource unused for this long, checked on next borrow.
max_lifetime None Replace a resource older than this, checked on next borrow.
reap_interval None Background prune cadence. None applies expiry lazily on borrow.
use std::time::Duration;
use pool_mod::{Manager, Pool};
# use std::convert::Infallible;
# struct M;
# impl Manager for M {
#   type Resource = (); type Error = Infallible;
#   fn create(&self) -> Result<(), Infallible> { Ok(()) }
#   fn recycle(&self, _r: &mut ()) -> Result<(), Infallible> { Ok(()) }
# }

let pool = Pool::builder(M)
    .max_size(32)
    .min_idle(4)
    .create_timeout(Some(Duration::from_secs(5)))
    .idle_timeout(Some(Duration::from_secs(600)))
    .max_lifetime(Some(Duration::from_secs(3600)))
    .build()
    .expect("configuration is valid");
# let _ = pool;

Using It From Async

The pool has no async dependency and get blocks the calling thread. In an async context, acquire on a blocking-friendly executor thread; the returned guard is Send, so it may be held across .await points:

let pool = pool.clone();
let mut conn = tokio::task::spawn_blocking(move || pool.get()).await??;
// `conn` is usable across awaits here.

A native non-blocking async acquisition API is on the roadmap (see below).


API Overview

For the complete reference — every public item, its parameters, return values, error semantics, and runnable examples — see docs/API.md.

  • Manager — the trait you implement: create, recycle, and the optional validate health check.
  • Poolbuilder / new, get / get_timeout / try_get, status, close, is_closed.
  • Builder — fluent configuration.
  • PoolConfig — limits and lifecycle policy.
  • Pooled — the RAII guard, deref-coercing to your resource.
  • Statussize, idle, in_use, max_size.
  • ErrorBackend, Timeout, Closed, InvalidConfig.

Performance

The hot path is borrow-and-return against an available resource. The pool guards only a small queue and a few counters under a mutex; resource construction, validation, and recycling all run outside the lock, and an uncontended check-in/return touches the condition variable only when a thread is actually waiting.

Latest local Criterion means (cargo bench, Windows x86_64, Rust stable, single-threaded, trivial resource):

Benchmark Time/op
get + return (reuse) ~98 ns
try_get + return ~97 ns
status snapshot ~8.5 ns

These measure the pool machinery itself with a no-op resource; in real use the checkout cost is dominated by the work the resource does (a query, a request), which the pool exists to amortize. The steady-state checkout/return path performs no heap allocation. Numbers vary by CPU and platform — run cargo bench on your target for figures that matter to you.


Cross-Platform Support

  • Linux (x86_64, aarch64)
  • macOS (x86_64, Apple Silicon)
  • Windows (x86_64)

CI runs formatting, lints, tests, and rustdoc with -D warnings on all three operating systems, across both the stable toolchain and the MSRV (1.75).


Testing

The suite covers every lifecycle path with unit tests, an eight-thread concurrency test, proptest properties for the pool's invariants (the max_size ceiling, the size == idle + in_use identity, reuse, and close semantics), and doctests on every public item.

# Full suite (unit, integration, property, and doctests)
cargo test --all-features

# Microbenchmarks for the acquire/return hot path
cargo bench

# Lints and formatting, as enforced in CI
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all -- --check

Stability

1.0.0 is the API freeze. Every public item is stable under semantic versioning: nothing is removed, renamed, or changed in a breaking way within the 1.x series. New functionality is additive, and Error is #[non_exhaustive] so it can grow without breaking matches. The MSRV (Rust 1.75) will not rise in a patch release. See docs/API.md for the full promise and CHANGELOG.md for the history.



Standards

  • REPS governs every decision. See REPS.md.
  • MSRV: Rust 1.75.
  • Edition: 2021.
  • Cross-platform: Linux, macOS, Windows.

License

Dual-licensed under either of:

at your option.

COPYRIGHT © 2025 JAMES GOBER.

About

Generic object and connection pooling. Async-safe with min/max sizing, idle timeouts, max-lifetime enforcement, validation-on-borrow, and health-check callbacks. Runtime-agnostic.

Topics

Resources

License

Apache-2.0, Unknown licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
Unknown
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages