Crate fastpool

Source
Expand description

Fastpool provides fast and runtime-agnostic object pools for Async Rust.

This crate provides two implementations: bounded pool and unbounded pool.

§Bounded pool

A bounded pool creates and recycles objects with full management. You cannot put an object to the pool manually.

The pool is bounded by the max_size config option of PoolConfig. If the pool reaches the maximum size, it will block all the Pool::get calls until an object is returned to the pool or an object is detached from the pool.

Bounded pools are useful for pooling database connections.

§Examples

Read the following simple demo or more complex examples in the examples directory.

use std::future::Future;

use fastpool::ManageObject;
use fastpool::ObjectStatus;
use fastpool::bounded::Pool;
use fastpool::bounded::PoolConfig;

struct Compute;
impl Compute {
    async fn do_work(&self) -> i32 {
        42
    }
}

struct Manager;
impl ManageObject for Manager {
    type Object = Compute;
    type Error = ();

    async fn create(&self) -> Result<Self::Object, Self::Error> {
        Ok(Compute)
    }

    async fn is_recyclable(
        &self,
        o: &mut Self::Object,
        status: &ObjectStatus,
    ) -> Result<(), Self::Error> {
        Ok(())
    }
}

let pool = Pool::new(PoolConfig::new(16), Manager);
let o = pool.get().await.unwrap();
assert_eq!(o.do_work().await, 42);

§Unbounded pool

An unbounded pool, on the other hand, allows you to put objects to the pool manually. You can use it like Go’s sync.Pool.

To configure a factory for creating objects when the pool is empty, like sync.Pool’s New, you can create the unbounded pool via Pool::new with an implementation of ManageObject.

§Examples

Read the following simple demo or more complex examples in the examples directory.

use fastpool::unbounded::Pool;
use fastpool::unbounded::PoolConfig;

let pool = Pool::<Vec<u8>>::never_manage(PoolConfig::default());

let result = pool.get().await;
assert_eq!(result.unwrap_err().to_string(), "unbounded pool is empty");

pool.extend_one(Vec::with_capacity(1024));
let o = pool.get().await.unwrap();
assert_eq!(o.capacity(), 1024);

§FAQ

§Why does fastpool have no timeout config?

Many async object pool implementations allow you to configure multiple timeout, like “wait timeout”, “create timeout”, “recycle timeout”, etc.

This introduces two major problems:

First, to support timeouts, the pool must use with a timer implementation like tokio::time. This would prevent the pool from being runtime-agnostic. Theoretically, the pool can depend on a timer trait, but there is no such a standard trait in the Rust ecosystem yet.

Second, timeouts options not only add complexity for configuration, but also the value itself cannot be configured properly at all. For example, end users often care about the total time used to obtain an object. This is not solely “wait timeout”, “create timeout”, or “recycle timeout”, but a conditional composition of all internal operations.

Thus, we propose a caller-side timeout solution:

use std::sync::Arc;
use std::time::Duration;

use fastpool::bounded::Object;
use fastpool::bounded::Pool;

#[derive(Debug, Clone)]
pub struct ConnectionPool {
    pool: Arc<Pool<ManageConnection>>,
}

impl ConnectionPool {
    pub async fn acquire(&self) -> Result<Object<ManageConnection>, Error> {
        const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60);

        // note that users can choose any timer implementation here
        let result = tokio::time::timeout(ACQUIRE_TIMEOUT, self.pool.get()).await;

        // ... processing the result
    }
}

Check out the postgres example in the examples directory for the complete code.

§Why does fastpool have no before/after hooks?

Similar to the second point of the previous question, the before/after hooks (closures) are very hard to configure properly. Specific to closures, many Rust code can be easily written in place, but if you’d like to pass a code block as a closure, then you may encounter a lot of lifetime and ownership issues. Besides, how to handle Result in the closure is also a headache.

Fastpool provides an ordinary interface of object pools, so that you should be able to add any before/after logic in the implementation or using a wrapper.

For example, all the “post-create”, “pre-recycle”, and “post-recycle” hooks can be implemented as:

use std::future::Future;

use fastpool::ManageObject;
use fastpool::ObjectStatus;

struct Manager;
impl ManageObject for Manager {
    type Object = i32;
    type Error = std::convert::Infallible;

    async fn create(&self) -> Result<Self::Object, Self::Error> {
        let o = 42;
        // any post-create hooks
        Ok(o)
    }

    async fn is_recyclable(
        &self,
        _o: &mut Self::Object,
        _status: &ObjectStatus,
    ) -> Result<(), Self::Error> {
        // any pre-recycle hooks
        // determinate if is_recyclable
        // any post-recycle hooks
        Ok(())
    }
}

Modules§

bounded
Bounded object pools.
unbounded
Unbounded object pools.

Structs§

ObjectStatus
Statistics regarding an object returned by the pool.
RetainResult
The result returned by Pool::retain.

Enums§

QueueStrategy
Queue strategy when deque objects from the object pool.

Traits§

ManageObject
A trait whose instance creates new objects and recycles existing ones.