Effortless Resource Management: Easy Object Pooling in Rust

Photo by Anna Nekrashevich: https://www.pexels.com/photo/red-fruit-on-white-textile-7214836/

Introduction

Sometimes, for reasons of efficiency, it can be quite handy to keep a pool, that is non-empty set, of initialized objects ready. This can be the case in for instance when you have database connections, which are expensive to create both in time and resources.

What you do is that make a pool of objects, usually an array, and you have one object managing that array, giving out objects, and returning, basically doing the bookkeeping.

Implementation in Rust

We will implement this pattern in a very simple and almost naive way. In this example we will be dealing with databaseconnections which only have one property, an id which is of type int16.

First the preliminaries:

use std::sync::{Arc,Mutex};

Since we might be dealing with different connections and for this example we need to be able to identify them, we have the following trait:

pub trait Identifiable {
    fn get_id(&self)->i16;
}

All this does, is define a method to return an id, which for simplicity is just an integer.

Next we need the concrete objects to fill the pool:

struct ConcreteConnection {
    id: i16,
}

impl ConcreteConnection {
    fn new(value: i16)->Self {
        ConcreteConnection {
            id: value
        }
    }
}

impl Identifiable for ConcreteConnection {
    fn get_id(&self) -> i16 {
        self.id
    }
}

A short description:

  1. The struct just has a an id field
  2. We define a constructor
  3. The get_id() function returns the id

Now come to the heart of the pattern, the Pool struct:

pub struct Pool<T> {
    pool: Arc<Mutex<Vec<T>>>,
}

We notice four things about it:

  1. The struct is generic with no type constraints.
  2. The objects are stored in a vector.
  3. This vector is wrapped in a Mutex so only one thread at a time can access it.
  4. This is wrapped in turn in an Arc struct, which provides threadsafe ownership of the underlying Mutex.

Now we need to implement the Pool object

The Pool constructor

The constructor looks like this:

impl<T> Pool<T> {
    pub fn new()->Self {
        Pool {
            pool: Arc::new(Mutex::new(Vec::new())),
        }
    }
}

This code is quite self-explanatory.

The CRUD methods

Next we to be able to add objects to the pool:

pub fn add(&mut self, value: T) {
        self.pool.lock().unwrap().push(value);
}

Some notes:

  • We need to lock the pool variable, in order to access it. This could go wrong, hence the unwrap. In production code we would probably add some error handling here.

Once we are done with an object, it is a good practice to return it to the pool:

    pub fn release(&mut self, value: T) {
        self.pool.lock().unwrap().push(value);
    }

Here we just push the object back onto the pool. Note that here we need to lock the Mutex before doing that.

Now comes the most important method, getting an object from the pool:

pub fn get(&mut self)->Option<T> {
        let mut pool=self.pool.lock().unwrap();
        if pool.len() > 0 {
            return pool.pop();
        }
        None
 }

Line by line:

  1. We get the pool and lock it.
  2. Next we check whether we have any objects left in the pool. If so, we pop the next object and return it.
  3. If not, we return a None value. Note that the return value of this method is an Option<T> type.

Note: make sure that the add(), release(), and get() methods are in the ‘impl<T> Pool<T>’ implementation block.

Time to test

Now we can define a main function and test it:

fn main() {
    let mut pool:Pool<Box<dyn Identifiable>> = Pool::<Box<dyn Identifiable>>::new();
    pool.add(Box::new(ConcreteConnection::new(1)));
    pool.add(Box::new(ConcreteConnection::new(2)));
    pool.add(Box::new(ConcreteConnection::new(3)));
    
    let connection=pool.get().unwrap();
    let connection2=pool.get().unwrap();

    println!("Connection id: {}", connection.get_id());
    println!("Connection id: {}", connection2.get_id());
}

A short description:

  1. We construct a pool, which holds objects with a certain trait implementation, Identifiable.
  2. Next we add some concrete objects. Note the use of the Box constructor.
  3. Then we get and print two connections.

So, not all that hard, and even in this simple form quite effective.

Conclusion

As you can see this is a very simple implementation. One thing that could be added is taking advantage of the Drop trait in Rust so as to return object automatically.

Also there are some crates like lockfree-object-pool, object-pool, and especially opool which also implement and extend this functionality. I will look at those in a later post.

As you can see, pre-baking objects can safe a lot of time. However, one caveat would be that many large object can weigh in your memory, so you might need to compromise between memory and performance.

Leave a Reply

Your email address will not be published. Required fields are marked *