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
}
3 Answers 3
A few random notes:
If I were you I'd check the source and API of Apache Commons Pool. You could borrow ideas about possible errors, corner cases, useful API calls, parameters etc.
surrenderConnection
should rollback unfinished transactions. They could leak.SLF4J has better API than JUL. (Check Logback also, if you don't familiar with it.)
A malicious (or poorly written client with multiple pools) can call
surrenderConnection
with aPooledConnection
instance which was not created by the calledConnectionPool
.I'd check
null
s in the constructors/methods. Does it make sense to call them withnull
? If not, check it and throw an exception. checkNotNull in Guava is a great choice for that.this.connectionPool = checkNotNull(connectionPool, "connectionPool cannot be null");
(Also see: Effective Java, 2nd edition, Item 38: Check parameters for validity)
-
2\$\begingroup\$ Awesome :) Point #2 and #4 would have caused serious bugs. Will fix 'em and update the code soon. \$\endgroup\$Amit Sharma– Amit Sharma2014年01月25日 07:41:29 +00:00Commented Jan 25, 2014 at 7:41
-
1\$\begingroup\$ I generally avoid other logging apis like SLF4J and use JUL to avoid additional library dependencies. Java EE servers also use JUL to capture and manage the logs as well. If you use a logging API then it becomes and application responsibility. \$\endgroup\$Archimedes Trajano– Archimedes Trajano2014年01月28日 16:35:46 +00:00Commented Jan 28, 2014 at 16:35
It looks like the borrow connection code will create a new connection without first checking to see if there is an available connection in the pool. That's not how most connection pools work. You probably want to store a count of available connections (consider using an AtomicInteger for safe concurrent access) and check against that before adding the new connection.
Other than that it looks pretty solid. However, it looks like your test coverage is pretty thin. I suggest beefing that up. Unit tests are an excellent way to make sure your code performs as expected.
-
\$\begingroup\$ I pushed in a quick fix to peek the queue before opening a new pooled-connection (in the inline code). Will give some more thought on using
AtomicInteger
and update the code. Thanks for the review :) \$\endgroup\$Amit Sharma– Amit Sharma2014年01月25日 07:33:50 +00:00Commented Jan 25, 2014 at 7:33
I didn't understand the concept here. How come ConnectionPool
is a part of PooledConnection
?
My understanding of a pool - you a bucket with 100 items, take one, use it and put it back into the pond.
I would prefer writing something like:
ConnectionPool.getConnection();
ConnectionPool.releaseConnection(Connection c);
Where there will be only 1 instance of this pool. (I would use a private constructor and a getInstance() for pool)
-
\$\begingroup\$ Making ConnectionPool a singleton (private constructor & singleton) is a bad idea. What if same client wants to have 2 connection pools for connecting to 2 urls? \$\endgroup\$avmohan– avmohan2019年03月28日 20:59:47 +00:00Commented Mar 28, 2019 at 20:59
-
\$\begingroup\$ In that case, you should take 2 connections from the same pool. You should not create multiple pools. \$\endgroup\$Ram B Gorre– Ram B Gorre2019年12月06日 05:20:11 +00:00Commented Dec 6, 2019 at 5:20
-
\$\begingroup\$ If an app needs to connect to both db1 & db2, the connections cannot be in the same pool. The connections are not interchangeable \$\endgroup\$avmohan– avmohan2019年12月06日 08:14:48 +00:00Commented Dec 6, 2019 at 8:14
-
\$\begingroup\$ OMG. dude, you didn't get it. A pool is for one db. For a different db, there should be a different pool. In such cases, you may need a pool factory to handover pool objects to you when you pass db info. \$\endgroup\$Ram B Gorre– Ram B Gorre2019年12月07日 09:08:18 +00:00Commented Dec 7, 2019 at 9:08
-
\$\begingroup\$ What I'm saying is - making it a true Singleton - with private constructor and all makes it impossible to make different pools per db. So the better option is to keep it non-jvm-singleton. It can be instantiated through a factory or a DI framework \$\endgroup\$avmohan– avmohan2019年12月07日 16:53:35 +00:00Commented Dec 7, 2019 at 16:53
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\$