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 Go
To continue on our database example, we will implement a very simple connectionpooling solution. The connections in this solution have only one attribute namely an integer id.
We will start as usual with the preliminaries:
package main
import (
"fmt"
"log"
"sync"
)
Since we want our objectpool to be threadsafe, we will need the Mutex struct to achieve that goal.
The objects in our pool maybe of different types, however what we need is for them to have a common interface, such as this:
type DbConnection interface {
getConnectionID() int16
}
Next we create a simple concrete type to put in our pool:
type ConcreteConnection struct {
id int16
}
func (c *ConcreteConnection) getConnectionID() int16 {
return c.id
}
This code needs no further explanation
The ConnectionPool
Next we come to the heart of this pattern, the ConnectionPool struct:
type ConnectionPool struct {
idleConnections []DbConnection
activeConnections []DbConnection
capacity int
poolLock *sync.Mutex
}
Line by line:
- We need a list of idle connection, i.e. connection which are not in active use.
- Also we need to keep track of the active connections
- Our pool has a maximum capacity. One nice extension of this pattern would be to have a flexible pool which grows and shrinks as needed.
- Next we a look on our pool, to make sure only one thread can access it at any given time
Initializing the Pool
The pool will need to be initialized:
func initializeConnectionPool(initialConnections []DbConnection) (*ConnectionPool, error) {
if len(initialConnections) == 0 {
return nil, fmt.Errorf("Cannot create an empty pool")
}
activeConnections := make([]DbConnection, 0)
result := &ConnectionPool{
idleConnections: initialConnections,
activeConnections: activeConnections,
capacity: len(initialConnections),
poolLock: new(sync.Mutex),
}
return result, nil
}
Some notes:
- The capacity is taken from the initial connections, which is a slice of already initialized connections.
- At the start we do not have any active connections, so that slice is initialized to a zero length
Getting connections
Next, we want to be able to get a connection from the pool:
func (c *ConnectionPool) getConnection() (DbConnection, error) {
c.poolLock.Lock()
defer c.poolLock.Unlock()
if len(c.idleConnections) == 0 {
return nil, fmt.Errorf("We found no free connections. Please retry later")
}
result := c.idleConnections[0]
c.idleConnections = c.idleConnections[1:]
c.activeConnections = append(c.activeConnections, result)
fmt.Printf("Gave out connection with ID: %d\n", result.getConnectionID())
return result, nil
}
Line by line:
- We lock the Mutex if possible
- We use the defer statement to unlock the mutex at the end of the function.
- We check whether we have any free connections, if not we return an error
- Else, we get the first idle connection
- This is added to the active connections
- And the idle connections slice is updated (the [1:] returns the whole array minus the first element)
Returning connections
Once we are done with the connection we need to return it:
func (c *ConnectionPool) returnConnection(connection DbConnection) error {
c.poolLock.Lock()
defer c.poolLock.Unlock()
err := c.removeFromActive(connection)
if err != nil {
return err
}
c.idleConnections = append(c.idleConnections, connection)
return nil
}
Line by line:
- Like in our previous function, we lock the mutex and unlock it using defer.
- We remove the connection from the active slice, and return an error if that does not succeed.
- Then we add the now idle slice to the idle connections.
In this bit of code we used the removeActive() function, to remove the current connection from the active connections. The implementation of that looks as follows:
func (c *ConnectionPool) removeFromActive(connection DbConnection) error {
activeLength := len(c.activeConnections)
for i, conn := range c.activeConnections {
if conn.getConnectionID() == connection.getConnectionID() {
c.activeConnections[activeLength-1], c.activeConnections[i] = c.activeConnections[i], c.activeConnections[activeLength-1]
c.activeConnections = c.activeConnections[:activeLength-1]
return nil
}
}
return fmt.Errorf("Could not find connection with ID %d in active connection pool")
}
A short explanation::
- We start iterating over all the elements in the active slice.
- If we find an element with the matching ID, we swap that element with the last element in the slice
- Then we update the slice to cut off the last element, removing the connection and keeping the active slice compact and of the correct length.
- In that case we also need to return nil for the error, since no error occurred.
- If we cannot find the element, we return an error with the appropiate element.
Time to test
Now we can start testing it:
func main() {
connections := make([]DbConnection, 2)
connections[0] = &ConcreteConnection{id: 1}
connections[1] = &ConcreteConnection{id: 2}
connectionPool, err := initializeConnectionPool(connections)
if err != nil {
log.Fatalf("Failed to initialize connectionPool: %v\n", err)
}
firstConnection, err := connectionPool.getConnection()
if err != nil {
log.Printf("Failed to get connection from Pool: %v\n", err)
} else {
fmt.Printf("Got first connection from pool: %d\n", firstConnection.getConnectionID())
}
secondConnection, err := connectionPool.getConnection()
if err != nil {
log.Printf("Failed to get connection from Pool: %v\n", err)
} else {
fmt.Printf("Got second connection from pool: %d\n", secondConnection.getConnectionID())
}
thirdConnection, err := connectionPool.getConnection()
if err != nil {
log.Printf("Failed to get connection from Pool: %v\n", err)
} else {
fmt.Printf("Got third connection from pool: %d\n", thirdConnection.getConnectionID())
}
err = connectionPool.returnConnection(firstConnection)
if err != nil {
log.Printf("Failed to return connection to Pool: %v\n", err)
}
err = connectionPool.returnConnection(secondConnection)
if err != nil {
log.Printf("Failed to return connection to Pool: %v\n", err)
}
thirdConnection, err = connectionPool.getConnection()
if err != nil {
log.Printf("Failed to get connection from Pool: %v\n", err)
} else {
fmt.Printf("Got third connection from pool (2nd try): %d\n", thirdConnection.getConnectionID())
}
}
What do we do here:
- We create pool with two connections.
- Next we try and get two connections, this should go without problems
- The third one should give a problem, and should log that.
- Once we returned two objects to the pool, getting the third one should be no problem
Conclusion
This is not the most difficult of patterns, yet it is very useful, and easy to understand. One of the problems with this pattern is the fact that getting things from the pool is easy, but the user of the API is never required to return the objects. Maybe I should look into finding a mechanism for that in a later post.