I wanted to try out writing an minimalistic connection pool implementation (out of curiosity and for learning). Could you all please review the code and provide feedback?
There are two classes (class names are hyperlinked to code):
com.amitcodes.dbcp.ConnectionPool
: The connection pool implementationcom.amitcodes.dbcp.PooledConnection
: The proxy forjava.sql.Connection
, written with the intent of ensuring that connections borrowed fromConnectionPool
should be not be closed by client code, but surrendered back to the pool.
TODOs (based on review comments):
This is why code reviews are important. I've added the following changes so far based on the code review comments:
- maintain a count of active connections available in the pool (preferably using
AtomicInteger
) borrowConnection()
should ensure there are no available idle connections (using point #1 above) before opening a new pooled connection- surrenderConnection should rollback unfinished transactions. They could leak.
- validate that surrendered connections are same as ones which were borrowed. If this check is not in place, a client can surrender connection to db 'foo' to dbcp for db 'bar'
surrenderConnection()
should take care of rolling back any open transactions- include a validate() method to validate constructor params
The code:
package com.amitcodes.dbcp;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ConnectionPool {
private static final Logger logger = Logger.getLogger(ConnectionPool.class.getCanonicalName());
private BlockingQueue<Connection> pool;
/**
* Maximum number of connections that the pool can have
*/
private int maxPoolSize;
/**
* Number of connections that should be created initially
*/
private int initialPoolSize;
/**
* Number of connections generated so far
*/
private int currentPoolSize;
private String dbUrl;
private String dbUser;
private String dbPassword;
public ConnectionPool(int maxPoolSize, int initialPoolSize, String url, String username,
String password, String driverClassName) throws ClassNotFoundException, SQLException {
if ((initialPoolSize > maxPoolSize) || initialPoolSize < 1 || maxPoolSize < 1) {
throw new IllegalArgumentException("Invalid pool size parameters");
}
// default max pool size to 10
this.maxPoolSize = maxPoolSize > 0 ? maxPoolSize : 10;
this.initialPoolSize = initialPoolSize;
this.dbUrl = url;
this.dbUser = username;
this.dbPassword = password;
this.pool = new LinkedBlockingQueue<Connection>(maxPoolSize);
initPooledConnections(driverClassName);
if (pool.size() != initialPoolSize) {
logger.log(Level.WARNING,
"Initial sized pool creation failed. InitializedPoolSize={0}, initialPoolSize={1}",
new Object[]{pool.size(), initialPoolSize});
}
}
private void initPooledConnections(String driverClassName)
throws ClassNotFoundException, SQLException {
// 1. Attempt to load the driver class
Class.forName(driverClassName);
// 2. Create and pool connections
for (int i = 0; i < initialPoolSize; i++) {
openAndPoolConnection();
}
}
private synchronized void openAndPoolConnection() throws SQLException {
if (currentPoolSize == maxPoolSize) {
return;
}
Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
pool.offer(new PooledConnection(conn, this));
currentPoolSize++;
logger.log(Level.FINE, "Created connection {0}, currentPoolSize={1}, maxPoolSize={2}",
new Object[]{conn, currentPoolSize, maxPoolSize});
}
public Connection borrowConnection() throws InterruptedException, SQLException {
if (pool.peek()==null && currentPoolSize < maxPoolSize) {
openAndPoolConnection();
}
// Borrowing thread will be blocked till connection
// becomes available in the queue
return pool.take();
}
public void surrenderConnection(Connection conn) {
if (!(conn instanceof PooledConnection)) {
return;
}
pool.offer(conn); // offer() as we do not want to go beyond capacity
}
}
com.amitcodes.dbcp.PooledConnection
: Only relevant section is posted here and boiler-plate code has been removed. You can view the complete class here on GitHub.
package com.amitcodes.dbcp;
import java.sql.*;
import java.util.*;
import java.util.concurrent.Executor;
public class PooledConnection implements Connection {
private Connection coreConnection;
private ConnectionPool connectionPool;
public PooledConnection(Connection coreConnection, ConnectionPool connectionPool) {
this.connectionPool = connectionPool;
this.coreConnection = coreConnection;
}
@Override
public void close() throws SQLException {
connectionPool.surrenderConnection(this);
}
/* ****************************************************************
* Proxy Methods
* ****************************************************************/
@Override
public Statement createStatement() throws SQLException {
return coreConnection.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return coreConnection.prepareStatement(sql);
}
// SOME CODE SKIPPED
}
surrenderConnection(connection)
but still keeps a reference to the conenction he can still go ahead and call methods likecreateStatement()
andprepareStatement()
even though the connection is returned to the poll that might me handed over to another client? How will we resolve this? \$\endgroup\$