Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

ATL OLEDB Memory Leak 2: CDynamicParameterAccessor

0.00/5 (No votes)
27 Apr 2009CPOL3 min read 21.3K  
A subtle memory leak problem on CDynamicParameterAccessor on special occasions

Introduction

The article reveals a tricky memory leak bug in the CDynamicParameterAccessor ATL OLEDB object. The article explains why it's happening and provides a solution for it. This is the continuation of the previous memory leak article on CCommand. Compared to CCommand, this bug leaks a small amount of memory and only under special circumstances. However, if a 24/7 server code leaks memory like this, it will sooner or later go out of memory.

Background

Microsoft Knowledge Base describes a well-known memory leak problem and the way to fix it. However, there is another delicate memory leak problem not seen reported anywhere. Most likely, nobody uses ATL OLEDB this way. However, it is a typical use scenario in my project as performance is critical.

Who would run the same command for a million times as in this example? I will. The idea is to prepare the command with parameters and run it with different parameters. We use it to write trading orders and other stuff during the day, real time to a SQL Server. This way, we will have the best performance as you don't need to generate the SQL string and prepare it again and again.

C++
CDataSource ds;//Create and open the data source
CDBPropSet dbinit(DBPROPSET_DBINIT);
dbinit.AddProperty(DBPROP_INIT_DATASOURCE, "192.168.60.18");
dbinit.AddProperty(DBPROP_AUTH_USERID, "user");
dbinit.AddProperty(DBPROP_AUTH_PASSWORD, "");
dbinit.AddProperty(DBPROP_INIT_CATALOG, "master");
dbinit.AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, false);
dbinit.AddProperty(DBPROP_INIT_LCID, 1033L);
dbinit.AddProperty(DBPROP_INIT_PROMPT, static_cast<short>(4));

HRESULT hr = ds.Open(_T("SQLOLEDB.1"), &dbinit);//Create Session.
CSession session;
session.Open(ds);
CCommand<CDynamicParameterAccessor, CRowset, CMultipleResults> command;
hr = command.Create(session, "exec sp_tables");
for(int i=0; i<1000000; i++ )
{
    hr = command.Open(NULL,NULL,false,0);
    command.Close();
    if( command.GetMultiplePtr() != NULL )
    {
    command.GetMultiplePtr()->Release();
    *command.GetMultiplePtrAddress() = NULL;
    }
}

If you copy the code in your project and run it (please use the correct user and password for your DB environment), you will find in the Task Manager that the memory usage increases in 10Ks a second although the major memory leak mentioned in another article is fixed here. The difference here is using CDynamicParameterAccessor instead of CDynamicAccessor. And this small memory leak is on CDynamicParameterAccessor, when used without any parameters. Obviously, if you are very careful and use CDynamicAccessor when no parameter is necessary and CDynamicParameterAccessor when the stored procedure has some parameters, you can work around this bug. However, there are cases you don't want to differentiate them, because you may wrap the command to a new class and want to use it regardless of whether the stored procedure has a parameter or not.

The Bug

CCommand::Open() calls CDynamicParameterAccessor::BindParameters(). This is the implementation of CDynamicParameterAccessor::BindParameters():

C++
HRESULT BindParameters(HACCESSOR* pHAccessor, ICommand* pCommand,
void** ppParameterBuffer, bool fBindLength = false, bool fBindStatus = false ) throw()
{
    // If we have already bound the parameters then just return
    // the pointer to the parameter buffer
    if (*pHAccessor != NULL)
    {
        *ppParameterBuffer = m_pParameterBuffer;
        return S_OK;
    }

    CComPtr<IAccessor> spAccessor;
    ATLENSURE_RETURN(pCommand != NULL);
    HRESULT hr = pCommand->QueryInterface(&spAccessor);
    if (FAILED(hr))
        return hr;

    // Try to bind parameters if available
    CComPtr<ICommandWithParameters> spCommandParameters;
    hr = pCommand->QueryInterface(&spCommandParameters);
    if (FAILED(hr))
        return hr;

    DB_UPARAMS ulParams = 0;
    CComHeapPtr<DBPARAMINFO> spParamInfo;
    LPOLESTR pNamesBuffer;
    // Get Parameter Information
    hr = spCommandParameters->GetParameterInfo(&ulParams, &spParamInfo,&pNamesBuffer);
    if (FAILED(hr))
        return hr;

    // Create the parameter information for binding
    hr = AllocateParameterInfo(ulParams);
    if (FAILED(hr))
    {
        CoTaskMemFree(pNamesBuffer);
        return hr;
    }

    DBBYTEOFFSET nOffset = 0;
    DBBYTEOFFSET nDataOffset = 0;
    DBBYTEOFFSET nLengthOffset = 0;
    DBBYTEOFFSET nStatusOffset = 0;
    DBBINDING* pCurrent = m_pParameterEntry;
    for (ULONG l=0; l<ulParams; l++)
    {
        m_pParameterEntry[l].eParamIO = 0;
        if (spParamInfo[l].dwFlags & DBPARAMFLAGS_ISINPUT)
            m_pParameterEntry[l].eParamIO |= DBPARAMIO_INPUT;
        if (spParamInfo[l].dwFlags & DBPARAMFLAGS_ISOUTPUT)
            m_pParameterEntry[l].eParamIO |= DBPARAMIO_OUTPUT;

        // if this is a BLOB, truncate column length to m_nBlobSize (like 8000 bytes)
        if( spParamInfo[l].ulParamSize > m_nBlobSize )
            spParamInfo[l].ulParamSize = m_nBlobSize;

        // if this is a string, recalculate column size in bytes
        DBLENGTH colLength = spParamInfo[l].ulParamSize;
        if (spParamInfo[l].wType == DBTYPE_STR)
            colLength += 1;

        if (spParamInfo[l].wType == DBTYPE_WSTR)
            colLength = colLength*2 + 2;

        // Calculate the column data offset
        nDataOffset = AlignAndIncrementOffset
		( nOffset, colLength, GetAlignment( spParamInfo[l].wType ) );
        if( fBindLength )
        {
            // Calculate the column length offset
            nLengthOffset = AlignAndIncrementOffset
		( nOffset, sizeof(DBLENGTH), __alignof(DBLENGTH) );
        }

        if( fBindStatus )
        {
            // Calculate the column status offset
            nStatusOffset = AlignAndIncrementOffset
		( nOffset, sizeof(DBSTATUS), __alignof(DBSTATUS) );
        }

        Bind(pCurrent, spParamInfo[l].iOrdinal, spParamInfo[l].wType,
            colLength, spParamInfo[l].bPrecision, spParamInfo[l].bScale,

        m_pParameterEntry[l].eParamIO, nDataOffset, nLengthOffset, nStatusOffset );
        pCurrent++;
        m_ppParamName[l] = pNamesBuffer;
        if (pNamesBuffer && *pNamesBuffer)
        {
            // Search for the NULL termination character
            while (*pNamesBuffer++)
                ;
        }
    }

    // Allocate memory for the new buffer

    m_pParameterBuffer = NULL;
    ATLTRY(m_pParameterBuffer = new BYTE[nOffset]);
    if (m_pParameterBuffer == NULL)
    {
        // Note that pNamesBuffer will be freed in the destructor
        // by freeing *m_ppParamName
        return E_OUTOFMEMORY;
    }

    *ppParameterBuffer = m_pParameterBuffer;
    m_nParameterBufferSize = nOffset;
    m_nParams = ulParams;
    BindEntries(m_pParameterEntry, ulParams, pHAccessor, nOffset, spAccessor);
    return S_OK;
}

The line in bold is the source of the problem.

C++
    if (*pHAccessor != NULL)
      {
                *ppParameterBuffer = m_pParameterBuffer;
                return S_OK;
      }

and

C++
BindEntries(m_pParameterEntry, ulParams, pHAccessor, nOffset, spAccessor);

The code checks if *pHAccessor is NULL. If it's not NULL, it means the parameters are already bound and there is no need to allocate parameter related memories and to bind the parameter accessor. This normally is fine. The only problem is, when stored procedure doesn't have parameters, BindEntries() fails. in this case, *pHAccessor is still NULL but all parameter related memories are allocated. Yes, because ulParams is 0 as no parameter is necessary for the stored procedure. However, AllocateParameterInfo() will new DBBINDING[0] and new OLECHAR*[0]. These operations actually allocated a good pointer, with the content length 0. In release mode, both consume 16 bytes of memory. In debug version 64 bytes. Since every time *pHAccessor is NULL, this small piece of memory is allocated every time the call is made. Only the memory allocated last time is actually freed in the destructor: ~CDynamicParameterAccessor().

C++
~CDynamicParameterAccessor()
{
    delete [] m_pParameterEntry;
    if (m_ppParamName != NULL)
    {
        CoTaskMemFree(*m_ppParamName);
        delete [] m_ppParamName;
    }
    delete m_pParameterBuffer;
};

Solution

Check the memory directly instead of checking accessor. To avoid modifying ATL code directly, we need to subclass CDynamicParameterAccessor:

C++
class CDynamicParameterAccessorEx : public CDynamicParameterAccessor
{
     public:
         HRESULT BindParameters(HACCESSOR* pHAccessor, ICommand* pCommand,
        void** ppParameterBuffer, bool fBindLength = false, 
				bool fBindStatus = false) throw()
    {
        if( NULL == m_pParameterBuffer )
        {
            return CDynamicParameterAccessor::BindParameters
		(pHAccessor, pCommand, ppParameterBuffer, fBindLength, fBindStatus);
        }
        else
        {
            *ppParameterBuffer = m_pParameterBuffer;
            return S_OK;
        }
    }
}

And you inherit from CCommand<CDynamicParameterAccessorEx, CRowset, CMultipleResults> command.

History

  • 27th April, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)