Description:
Connection.prepareStatement uses the ServerPreparedStatement constructor, which registers the preparedStatement in the connections openStatements map. If the serverPrepare() call fails (in my case this happens because I use a LIMIT ? in my sql) then the prepared statement is not unregistered from the connection. This causes a memory leak, as the openStatements map grows continuously.
This behaviour is hard to find, because the connection.prepareStatement() method catches the exception and emulates the unsupported prepared statement with a clientPrepareStatement call.
How to repeat:
Use a prepared statement with a LIMIT ? clause. prepare this statement multiple times against the same connection. The connection's openStatements map will grow and the ServerPreparedStatement instances will be kept on the heap.
Suggested fix:
Proper error handling in ServerPreparedStatement constructor - if it throws an exception, it should leave things the way it found them.
I have implemented this, and it fixes the problem for me.
In the Statement constructor, move:
if (!this.connection.getDontTrackOpenResources()) {
this.connection.registerStatement(this);
}
to the end of the method - so if anything in the constructor fails, the statement is not registered with the connection.
the ServerPreparedStatement reads:
public ServerPreparedStatement(Connection conn, String sql, String catalog)
throws SQLException {
super(conn, catalog);
try
{
checkNullOrEmptyQuery(sql);
this.isSelectQuery = StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT"); //$NON-NLS-1$
this.useTrueBoolean = this.connection.versionMeetsMinimum(3,
21, 23);
this.hasLimitClause = (StringUtils.indexOfIgnoreCase(sql, "LIMIT") != -1); //$NON-NLS-1$
this.firstCharOfStmt = StringUtils.firstNonWsCharUc(sql);
this.originalSql = sql;
if (this.connection.versionMeetsMinimum(4, 1, 2)) {
this.stringTypeCode = MysqlDefs.FIELD_TYPE_VAR_STRING;
} else {
this.stringTypeCode = MysqlDefs.FIELD_TYPE_STRING;
}
serverPrepare(sql);
}
catch(Exception ex)
{
try
{
realClose(false);
}
catch(SQLException e)
{
conn.getLog().logWarn("Prepare statement failed and then couldn't clean up server prepared statement",e);
}
if(ex instanceof SQLException)
throw (SQLException)ex;
throw new SQLException(ex.toString(),
SQLError.SQL_STATE_GENERAL_ERROR);
}
}
Description: Connection.prepareStatement uses the ServerPreparedStatement constructor, which registers the preparedStatement in the connections openStatements map. If the serverPrepare() call fails (in my case this happens because I use a LIMIT ? in my sql) then the prepared statement is not unregistered from the connection. This causes a memory leak, as the openStatements map grows continuously. This behaviour is hard to find, because the connection.prepareStatement() method catches the exception and emulates the unsupported prepared statement with a clientPrepareStatement call. How to repeat: Use a prepared statement with a LIMIT ? clause. prepare this statement multiple times against the same connection. The connection's openStatements map will grow and the ServerPreparedStatement instances will be kept on the heap. Suggested fix: Proper error handling in ServerPreparedStatement constructor - if it throws an exception, it should leave things the way it found them. I have implemented this, and it fixes the problem for me. In the Statement constructor, move: if (!this.connection.getDontTrackOpenResources()) { this.connection.registerStatement(this); } to the end of the method - so if anything in the constructor fails, the statement is not registered with the connection. the ServerPreparedStatement reads: public ServerPreparedStatement(Connection conn, String sql, String catalog) throws SQLException { super(conn, catalog); try { checkNullOrEmptyQuery(sql); this.isSelectQuery = StringUtils.startsWithIgnoreCaseAndWs(sql, "SELECT"); //$NON-NLS-1$ this.useTrueBoolean = this.connection.versionMeetsMinimum(3, 21, 23); this.hasLimitClause = (StringUtils.indexOfIgnoreCase(sql, "LIMIT") != -1); //$NON-NLS-1$ this.firstCharOfStmt = StringUtils.firstNonWsCharUc(sql); this.originalSql = sql; if (this.connection.versionMeetsMinimum(4, 1, 2)) { this.stringTypeCode = MysqlDefs.FIELD_TYPE_VAR_STRING; } else { this.stringTypeCode = MysqlDefs.FIELD_TYPE_STRING; } serverPrepare(sql); } catch(Exception ex) { try { realClose(false); } catch(SQLException e) { conn.getLog().logWarn("Prepare statement failed and then couldn't clean up server prepared statement",e); } if(ex instanceof SQLException) throw (SQLException)ex; throw new SQLException(ex.toString(), SQLError.SQL_STATE_GENERAL_ERROR); } }