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.
CDataSource ds;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);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()
:
HRESULT BindParameters(HACCESSOR* pHAccessor, ICommand* pCommand,
void** ppParameterBuffer, bool fBindLength = false, bool fBindStatus = false ) throw()
{
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;
CComPtr<ICommandWithParameters> spCommandParameters;
hr = pCommand->QueryInterface(&spCommandParameters);
if (FAILED(hr))
return hr;
DB_UPARAMS ulParams = 0;
CComHeapPtr<DBPARAMINFO> spParamInfo;
LPOLESTR pNamesBuffer;
hr = spCommandParameters->GetParameterInfo(&ulParams, &spParamInfo,&pNamesBuffer);
if (FAILED(hr))
return hr;
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( spParamInfo[l].ulParamSize > m_nBlobSize )
spParamInfo[l].ulParamSize = m_nBlobSize;
DBLENGTH colLength = spParamInfo[l].ulParamSize;
if (spParamInfo[l].wType == DBTYPE_STR)
colLength += 1;
if (spParamInfo[l].wType == DBTYPE_WSTR)
colLength = colLength*2 + 2;
nDataOffset = AlignAndIncrementOffset
( nOffset, colLength, GetAlignment( spParamInfo[l].wType ) );
if( fBindLength )
{
nLengthOffset = AlignAndIncrementOffset
( nOffset, sizeof(DBLENGTH), __alignof(DBLENGTH) );
}
if( fBindStatus )
{
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)
{
while (*pNamesBuffer++)
;
}
}
m_pParameterBuffer = NULL;
ATLTRY(m_pParameterBuffer = new BYTE[nOffset]);
if (m_pParameterBuffer == NULL)
{
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.
if (*pHAccessor != NULL)
{
*ppParameterBuffer = m_pParameterBuffer;
return S_OK;
}
and
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()
.
~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
:
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