Pools — Reusing Expensive Resources

Creating a database connection takes time: DNS lookup, TCP handshake, TLS, authentication. Creating one per request is wasteful. A pool maintains a set of connections and lends them out, returning them when done.

id_effect provides Pool and KeyedPool as first-class effect constructs.

Pool: Basic Connection Pool

#![allow(unused)]
fn main() {
use id_effect::Pool;

// Create a pool of up to 10 connections
let pool: Pool<Connection> = Pool::new(
    || open_connection("postgres://localhost/app"),  // factory
    10,                                               // max size
);
}

The pool lazily creates connections up to the max. Idle connections are kept alive for reuse.

Using a Pool Connection

#![allow(unused)]
fn main() {
pool.with_resource(|conn: &Connection| {
    effect! {
        let rows = ~ conn.query("SELECT * FROM users");
        rows.into_iter().map(User::from_row).collect::<Vec<_>>()
    }
})
}

with_resource acquires a connection from the pool, runs the effect, and returns the connection automatically when done — regardless of success, failure, or cancellation. No acquire_release boilerplate; the pool handles it.

Waiting for Availability

If all connections are in use, with_resource waits until one becomes available:

#![allow(unused)]
fn main() {
// Concurrent requests share the pool; each waits its turn
fiber_all(vec![
    pool.with_resource(|c| query_a(c)),
    pool.with_resource(|c| query_b(c)),
    pool.with_resource(|c| query_c(c)),
])
}

The pool queues waiters and notifies them as connections are returned.

KeyedPool: Multiple Named Pools

For scenarios with multiple distinct pools (e.g., read replica + write primary):

#![allow(unused)]
fn main() {
use id_effect::KeyedPool;

let pools: KeyedPool<&str, Connection> = KeyedPool::new(
    |key: &&str| open_connection(key),
    5,  // max per key
);

// Get a connection for the write primary
pools.with_resource("write-primary", |conn| { ... })

// Get a connection for the read replica
pools.with_resource("read-replica", |conn| { ... })
}

Each key has its own independently bounded pool.

Pool as a Service

In practice, pools live in the effect environment as services:

#![allow(unused)]
fn main() {
service_key!(DbPoolKey: Pool<Connection>);

fn query_users() -> Effect<Vec<User>, DbError, impl NeedsDbPool> {
    effect! {
        let pool = ~ DbPoolKey;
        ~ pool.with_resource(|conn| {
            effect! {
                let rows = ~ conn.query("SELECT * FROM users");
                rows.iter().map(User::from_row).collect::<Vec<_>>()
            }
        })
    }
}
}

The pool is provided via a Layer:

#![allow(unused)]
fn main() {
let pool_layer = LayerFn::new(|config: &Tagged<ConfigKey>| {
    effect! {
        let pool = Pool::new(
            || Connection::open(config.value().db_url()),
            config.value().pool_size(),
        );
        tagged::<DbPoolKey>(pool)
    }
});
}

Pool creation, lifecycle, and cleanup are all handled by the Layer. Business code sees only NeedsDbPool.