Bug #10144 Memory leak in ServerPreparedStatement if serverPrepare fails
Submitted: 25 Apr 2005 13:57 Modified: 27 Apr 2005 18:06
Reporter: Eoin Curran Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / J Severity:S3 (Non-critical)
Version:3.1.8 OS:Microsoft Windows (w2k)
Assigned to: Mark Matthews CPU Architecture:Any

[25 Apr 2005 13:57] Eoin Curran
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);
        }
    }
[27 Apr 2005 18:06] Mark Matthews
Fixed for 3.1.9. The fix should be in tonight's nightly snapshot after 00:00 GMT at http://downloads.mysql.com/snapshots.php