Affects Version/s: 2.0
Fix Version/s: 2.0.1
I've found a case where a PoolablePreparedStatement that's currently in active use can get closed (resulting in "PoolablePreparedStatement with address: ... is closed" SQLException to the application), from a finalizer of another DelegatingPreparedStatement (that's obviously not in use, since it's being garbage collected).
0. Starting point: application has a Connection (it's a PoolGuardConnectionWrapper wrapping PoolableConnection wrapping PoolingConnection wrapping com.mysql.jdbc.JDBC4Connection), statement pooling is enabled, and prepareStatement has never been called
1. Application calls conn.prepareStatement. The statement pool is empty, so this ends up in PoolingConnection.makeObject, which then calls MySQL to get a PreparedStatement (stmt1), and wraps it in PoolablePreparedStatement (stmt2). PoolableConnection.prepareStatement wraps it in new DelegatingPreparedStatement (stmt3), and PoolGuardConnectionWrapper wraps it again (stmt4).
2. Application does something with stmt4, and eventually calls stmt4.close(). The call ends up in stmt2.close (in PoolablePreparedStatement) which returns stmt2 to the statement pool. Stmt2.passivate() gets called, setting the _closed flag to true.
3. Application forgets all about stmt4
4. Later, application calls conn.prepareStatement (with same SQL) again. The result is the old MySQL PreparedStatement (stmt1) wrapped in the old PoolablePreparedStatement (stmt2) wrapped in new DelegatingPreparedStatement stmt5 wrapped in new DelegatingPreparedStatement stmt6. Stmt2.activate() gets called, setting the _closed flag to false.
5. Now, garbage collection gets run, and stmt4 gets gc'd. stmt4.finalize() calls stmt4.close() which calls stmt3.close() which calls stmt2.close(). Since stmt2 is not closed any more (_closed was just set to false above), it gets closed and returned to the pool.
6. Applications calls something in stmt6, which ends up in stmt2, and stmt2.checkOpen() throws exception java.sql.SQLException: org.apache.commons.dbcp2.PoolablePreparedStatement with address: "com.mysql.jdbc.ServerPreparedStatement - select 1" is closed.
PoolGuardConnectionWrapper prevents the application from accidentally calling stmt4.close() later again, but this doesn't prevent the finalizer.... DBCP 1.4 did not have any finalizers, so it doesn't have this bug.
I guess DelegatingStatement.close() should check isClosed(), and in the finally block, also do setDelegate(null) (like PoolGuardConnectionWrapper does), to make sure the delegate (which might be in active use by someone else) can never get called via this DelegatingStatement again (although most methods in DelegatingStatement start with checkOpen(), not all of them do!).