windows-nt/Source/XPSP1/NT/base/cluster/service/lm/xsaction.c

833 lines
24 KiB
C
Raw Normal View History

2020-09-26 03:20:57 -05:00
/*++
Copyright (c) 1992-1997 Microsoft Corporation
Module Name:
xsaction.c
Abstract:
Provides basic xscational services for cluster logging.
Author:
Sunita Shrivastava (sunitas) 17-Mar-1997
Revision History:
--*/
#include "service.h"
#include "lmp.h"
/****
@doc EXTERNAL INTERFACES CLUSSVC LM
****/
/****
@func HXSACTION | LogStartXsaction| Write a start transaction record to
the log.
@parm IN HLOG | hLog | Supplies the handle to the log.
@parm IN TRID | TrId | Supplies the transaction id.
@parm IN RMID | ResourceId | The resource id that identifies the
resource manager.
@parm IN RMTYPE | ResourceFlags | A dword of flags that the resource
manager may use to store any data it wants with this record.
@rdesc Returns a handle suitable for use in subsequent log calls.
NUll in case failure. Call GetLastError() to get the error.
@xref <f LogCommitXsaction> <f LogAbortXsaction>
****/
HXSACTION
LogStartXsaction(
IN HLOG hLog,
IN TRID TrId,
IN RMID ResourceId,
IN RMTYPE ResourceFlags
)
{
PLOG pLog;
DWORD dwError=ERROR_SUCCESS;
LSN Lsn = NULL_LSN;
PLOGRECORD pLogRecord;
DWORD dwNumPages;
PLOGPAGE pPage;
DWORD dwTotalSize;
BOOL bMaxFileSizeReached;
PXSACTION pXsaction = NULL;
GETLOG(pLog, hLog);
//write the record, dont allow resets to happen
ClRtlLogPrint(LOG_NOISE,
"[LM] LogStartXsaction : Entry TrId=%1!u! RmId=%2!u! RmType = %3!u!\r\n",
TrId, ResourceId, ResourceFlags);
EnterCriticalSection(&pLog->Lock);
pXsaction = (PXSACTION)LocalAlloc(LMEM_FIXED, sizeof(XSACTION));
if (!pXsaction)
{
dwError = ERROR_NOT_ENOUGH_MEMORY;
goto FnExit;
}
#if DBG
{
DWORD dwOldProtect;
DWORD Status;
BOOL VPWorked;
VPWorked = VirtualProtect(pLog->ActivePage, pLog->SectorSize, PAGE_READWRITE, &dwOldProtect);
Status = GetLastError();
CL_ASSERT( VPWorked );
}
#endif
//reset the file
dwError = LogReset(hLog);
if (dwError != ERROR_SUCCESS)
{
ClRtlLogPrint(LOG_NOISE,
"[LM] LogStartXsaction : LogReset failed\r\n");
goto FnExit;
}
#if DBG
{
DWORD dwOldProtect;
DWORD Status;
BOOL VPWorked;
VPWorked = VirtualProtect(pLog->ActivePage, pLog->SectorSize, PAGE_READWRITE, &dwOldProtect);
Status = GetLastError();
CL_ASSERT( VPWorked );
}
#endif
CL_ASSERT(ResourceId > RMAny); // reserved for logger's use
dwTotalSize = sizeof(LOGRECORD) + 7 & ~7; // round up to qword size
pPage = LogpAppendPage(pLog, dwTotalSize, &pLogRecord, &bMaxFileSizeReached, &dwNumPages);
//we just reset the file, if it cant take the new startxsaction
//record, something is awfully wrong !!!
if (pPage == NULL)
{
dwError = GetLastError();
ClRtlLogPrint(LOG_NOISE,
"[LM] LogStartXsaction : LogpAppendPage failed.\r\n");
goto FnExit;
}
CL_ASSERT(((ULONG_PTR)pLogRecord & 0x7) == 0); // ensure qword alignment
Lsn = MAKELSN(pPage, pLogRecord);
//
// Fill in log record.
//
pLogRecord->Signature = LOGREC_SIG;
pLogRecord->ResourceManager = ResourceId;
pLogRecord->Transaction = TrId;
pLogRecord->XsactionType = TTStartXsaction;
pLogRecord->Flags = ResourceFlags;
GetSystemTimeAsFileTime(&pLogRecord->Timestamp);
pLogRecord->NumPages = dwNumPages;
pLogRecord->DataSize = 0;
pXsaction->XsactionSig = XSACTION_SIG;
pXsaction->TrId = TrId;
pXsaction->StartLsn = Lsn;
pXsaction->RmId = ResourceId;
FnExit:
#if DBG
{
DWORD dwOldProtect;
DWORD Status;
BOOL VPWorked;
VPWorked = VirtualProtect(pLog->ActivePage, pLog->SectorSize, PAGE_READONLY, & dwOldProtect);
Status = GetLastError();
CL_ASSERT( VPWorked );
}
#endif
if (dwError != ERROR_SUCCESS)
{
if (pXsaction) {
LocalFree(pXsaction);
pXsaction = NULL;
}
SetLastError(dwError);
}
LeaveCriticalSection(&pLog->Lock);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogStartXsaction : Exit returning=0x%1!08lx!\r\n",
Lsn);
return((HXSACTION)pXsaction);
}
/****
@func LSN | LogWriteXsaction| Write a transaction unit record to
the log.
@parm IN HLOG | hLog | Supplies the handle to the log.
@parm IN HXSACTION | hXsaction | Supplies the handle to the transaction.
@parm IN RMTYPE | ResourceFlags | A dword of flags that the resource
manager may use to store any data it wants with this record.
@parm IN PVOID | LogData | Supplies a pointer to the data to be logged.
@parm DWORD | DataSize | Supplies the number of bytes of data pointed to by LogData
@rdesc The LSN of the log record that was created. NULL_LSN if something terrible happened.
GetLastError() will provide the error code.
@comm This should use a transaction handle obtained from LogStartXsaction. This
call is used to write the parts of a transaction to the quorum log.
@xref <f LogStartXsaction>
****/
LSN
LogWriteXsaction(
IN HLOG hLog,
IN HXSACTION hXsaction,
IN RMTYPE ResourceFlags,
IN PVOID pLogData,
IN DWORD dwDataSize)
{
PLOG pLog;
DWORD dwError=ERROR_SUCCESS;
LSN Lsn = NULL_LSN;
PLOGRECORD pLogRecord;
DWORD dwNumPages;
PLOGPAGE pPage;
DWORD dwTotalSize;
BOOL bMaxFileSizeReached;
PXSACTION pXsaction = NULL;
GETLOG(pLog, hLog);
GETXSACTION(pXsaction, hXsaction);
//write the record, dont allow resets to happen
ClRtlLogPrint(LOG_NOISE,
"[LM] LogWriteXsaction : Entry TrId=%1!u! RmId=%2!u! RmType = %3!u!\r\n",
pXsaction->TrId, pXsaction->RmId, ResourceFlags);
#if DBG
{
DWORD dwOldProtect;
DWORD Status;
BOOL VPWorked;
VPWorked = VirtualProtect(pLog->ActivePage, pLog->SectorSize, PAGE_READWRITE, &dwOldProtect);
Status = GetLastError();
CL_ASSERT( VPWorked );
}
#endif
CL_ASSERT(pXsaction->RmId > RMAny); // reserved for logger's use
dwTotalSize = sizeof(LOGRECORD) + (dwDataSize+ 7) & ~7; // round up to qword size
EnterCriticalSection(&pLog->Lock);
pPage = LogpAppendPage(pLog, dwTotalSize, &pLogRecord, &bMaxFileSizeReached, &dwNumPages);
//we reset the file in logstartxsaction, if it cant take the new startxsaction
//record, something is awfully wrong !!!
if (pPage == NULL)
{
dwError = GetLastError();
//assert if a complete local xsaction extends the log beyond its max size
CL_ASSERT( dwError != ERROR_CLUSTERLOG_EXCEEDS_MAXSIZE);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogWriteXsaction : LogpAppendPage failed.\r\n");
goto FnExit;
}
CL_ASSERT(((ULONG_PTR)pLogRecord & 0x7) == 0); // ensure qword alignment
Lsn = MAKELSN(pPage, pLogRecord);
//
// Fill in log record.
//
pLogRecord->Signature = LOGREC_SIG;
pLogRecord->ResourceManager = pXsaction->RmId;
pLogRecord->Transaction = pXsaction->TrId;
pLogRecord->XsactionType = TTXsactionUnit;
pLogRecord->Flags = ResourceFlags;
GetSystemTimeAsFileTime(&pLogRecord->Timestamp);
pLogRecord->NumPages = dwNumPages;
pLogRecord->DataSize = dwDataSize;
if (dwNumPages < 1)
CopyMemory(&pLogRecord->Data, pLogData, dwDataSize);
else
{
if (LogpWriteLargeRecordData(pLog, pLogRecord, pLogData, dwDataSize)
!= ERROR_SUCCESS)
{
ClRtlLogPrint(LOG_NOISE,
"[LM] LogWriteXsaction : LogpWriteLargeRecordData failed. Lsn=0x%1!08lx!\r\n",
Lsn);
Lsn = NULL_LSN;
}
}
FnExit:
#if DBG
{
DWORD dwOldProtect;
DWORD Status;
BOOL VPWorked;
VPWorked = VirtualProtect(pLog->ActivePage, pLog->SectorSize, PAGE_READONLY, & dwOldProtect);
Status = GetLastError();
CL_ASSERT( VPWorked );
}
#endif
if (dwError != ERROR_SUCCESS)
SetLastError(dwError);
LeaveCriticalSection(&pLog->Lock);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogWriteXsaction : Exit returning=0x%1!08lx!\r\n",
Lsn);
return(Lsn);
}
/****
@func DWORD | LogCommitXsaction | This writes the commit transaction
record to the log and flushes it.
@parm IN HLOG | hLog | Supplies the handle to the log.
@parm IN TRID | TrId | Supplies the transaction id.
@parm IN RMID | ResourceId | The resource id that identifies the
resource manager.
@parm IN RMTYPE | ResourceFlags | A dword of flags that the resource
manager may use to store any data it wants with this record.
@comm A commit record is written to the quorum log. The hXsaction handle
is invalidated at this point and should not be used after this call has
been made. The commit is record is used to identify committed transactions
during rollback.
@rdesc ERROR_SUCCESS if successful. Win32 error code if something horrible happened.
@xref <f LogStartXsaction>
****/
DWORD WINAPI LogCommitXsaction(
IN HLOG hLog,
IN HXSACTION hXsaction,
IN RMTYPE ResourceFlags)
{
DWORD dwError = ERROR_SUCCESS;
LSN Lsn;
PXSACTION pXsaction;
ClRtlLogPrint(LOG_NOISE,
"[LM] LogCommitXsaction : Entry, hXsaction=0x%1!08lx!\r\n",
hXsaction);
GETXSACTION(pXsaction, hXsaction);
Lsn = LogWrite(hLog, pXsaction->TrId, TTCommitXsaction,
pXsaction->RmId, ResourceFlags, NULL, 0);
if (Lsn == NULL_LSN)
{
dwError = GetLastError();
goto FnExit;
}
FnExit:
//free up the transation memory
ZeroMemory(pXsaction, sizeof(XSACTION)); // just in case somebody tries to
LocalFree(pXsaction);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogCommitXsaction : Exit, dwError=0x%1!08lx!\r\n",
dwError);
return(dwError);
}
/****
@func DWORD | LogAbortXsaction | Marks a given transaction as aborted in the
quorum log file.
@parm IN HLOG | hLog | Supplies the handle to the log.
@parm IN HXSACTION | hXsaction | Supplies the handle to the transaction.
@parm IN RMTYPE | ResourceFlags | A dword of flags that the resource
manager may use to store any data it wants with this record.
@comm An abort transaction is written to the quorum log. This is used in
identifying aborted transactions during roll back. The hXsaction
handle is invalidated at this point and should not be used after this.
@rdesc ERROR_SUCCESS if successful. Win32 error code if something horrible happened.
@xref <f LogStartXsaction> <f LogCommitXsaction>
****/
DWORD
LogAbortXsaction(
IN HLOG hLog,
IN HXSACTION hXsaction,
IN RMTYPE ResourceFlags
)
{
PXSACTION pXsaction;
LSN Lsn;
DWORD dwError = ERROR_SUCCESS;
ClRtlLogPrint(LOG_NOISE,
"[LM] LogAbortXsaction : Entry, hXsaction=0x%1!08lx!\r\n",
hXsaction);
GETXSACTION(pXsaction, hXsaction);
Lsn = LogWrite(hLog, pXsaction->TrId, TTAbortXsaction, pXsaction->RmId,
ResourceFlags, NULL, 0);
if (Lsn == NULL_LSN)
{
dwError = GetLastError();
goto FnExit;
}
FnExit:
ZeroMemory(pXsaction, sizeof(XSACTION)); // just in case somebody tries to
LocalFree(pXsaction);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogAbortXsaction : Exit, returning, dwError=0x%1!08lx!\r\n",
dwError);
return(dwError);
}
/****
@func LSN | LogFindXsactionState | This fuctions scans the record and finds
the state of a given transaction.
@parm IN HLOG | hLog | Supplies the identifier of the log.
@parm IN LSN | StartXsactionLsn | The LSN of the start transaction record.
@parm IN TRID | XsactionId | The transaction id of the transaction.
@parm OUT TRSTATE | *pXsactionState | The state of the transaction.
@comm Transaction state is set to XsactionCommitted, XsactionAborted or XsactionUnknown.
depending on whether a commit record, or abort record or no record is found
for this record in the log.
@rdesc ERROR_SUCCESS, else returns the error code if something horrible happens.
@xref <f LogScanXsaction>
****/
DWORD
LogFindXsactionState(
IN HLOG hLog,
IN LSN StartXsactionLsn,
IN TRID XsactionId,
OUT TRSTATE *pXsactionState)
{
PLOG pLog;
PLOGRECORD pRecord, pEopRecord;
DWORD dwError = ERROR_SUCCESS;
int PageIndex, OldPageIndex;
RMID Resource;
TRID TrId;
TRTYPE TrType;
LSN Lsn, EopLsn;
PLOGPAGE pPage = NULL,pLargeBuffer = NULL;
DWORD dwBytesRead;
RMTYPE ResourceFlags;
BOOL bFound = FALSE;
GETLOG(pLog, hLog);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogWrite : Entry StartXLsn=0x%1!08lx! StartXId=%2!u!\r\n",
StartXsactionLsn, XsactionId);
EnterCriticalSection(&pLog->Lock);
if (StartXsactionLsn >= pLog->NextLsn)
{
dwError = ERROR_INVALID_PARAMETER;
goto FnExit;
}
//read this record
dwBytesRead = 0;
if ((Lsn = LogRead( hLog, StartXsactionLsn, &Resource, &ResourceFlags, &TrId, &TrType,
NULL, &dwBytesRead)) == NULL_LSN)
{
dwError = GetLastError();
goto FnExit;
}
//check the record
if ((TrType != TTStartXsaction) ||
(TrId != XsactionId))
{
dwError = ERROR_INVALID_PARAMETER;
goto FnExit;
}
pPage = (PLOGPAGE)AlignAlloc(SECTOR_SIZE);
if (pPage == NULL) {
CL_UNEXPECTED_ERROR( ERROR_NOT_ENOUGH_MEMORY );
}
//Lsn is now set to the next Lsn after the start
//initialize this to -1 so that the first page is always read
OldPageIndex = -1;
while (Lsn < pLog->NextLsn && !bFound)
{
//
// Scan From Next record to find either the commit or abort record
//
PageIndex = LSNTOPAGE(Lsn);
if (PageIndex != OldPageIndex)
{
//read the page
pLog->Overlapped.Offset = PageIndex * pLog->SectorSize;
pLog->Overlapped.OffsetHigh = 0;
dwError = LogpRead(pLog, pPage, pLog->SectorSize, &dwBytesRead);
if (dwError)
{
goto FnExit;
}
//read was successful, no need to read the page unless the
//record falls on a different page
OldPageIndex = PageIndex;
}
pRecord = LSNTORECORD(pPage, Lsn);
//skip other log management records
//these are small records by definition
if (pRecord->ResourceManager < RMAny)
{
Lsn = GETNEXTLSN(pRecord, TRUE);
continue;
}
//if the transaction id is the same, check the xsaction type
if (pRecord->Transaction == XsactionId)
{
if ((pRecord->XsactionType == TTCommitXsaction) ||
(pRecord->XsactionType == TTStartXsaction))
{
bFound = TRUE;
continue;
}
}
//handle large records
if (pRecord->NumPages > 0)
{
EopLsn = GETNEXTLSN(pRecord,TRUE);
PageIndex = LSNTOPAGE(EopLsn);
//read the page
pLog->Overlapped.Offset = PageIndex * pLog->SectorSize;
pLog->Overlapped.OffsetHigh = 0;
dwError = LogpRead(pLog, pPage, pLog->SectorSize, &dwBytesRead);
if (dwError)
{
goto FnExit;
}
OldPageIndex = PageIndex;
pEopRecord = (PLOGRECORD)((ULONG_PTR) pPage +
(EopLsn - (pLog->Overlapped).Offset));
//move to the next page
Lsn = GETNEXTLSN(pEopRecord, TRUE);
}
else
{
Lsn = GETNEXTLSN(pRecord, TRUE);
}
}
if (bFound)
{
if (pRecord->XsactionType == TTCommitXsaction)
*pXsactionState = XsactionCommitted;
else
*pXsactionState = XsactionAborted;
}
else
{
*pXsactionState = XsactionUnknown;
}
ClRtlLogPrint(LOG_NOISE,
"[LM] LogFindXsactionState : Exit,State=%1!u!\r\n",
*pXsactionState);
FnExit:
LeaveCriticalSection(&pLog->Lock);
if (pPage) AlignFree(pPage);
return(dwError);
}
/****
@func LSN | LogScanXsaction | This fuctions scans the multiple units
of a transaction.
@parm IN HLOG | hLog | Supplies the identifier of the log.
@parm IN LSN | StartXsacionLsn | The LSN of the start transaction record.
@parm IN TRID | XsactionId | The transaction id of the transaction.
@parm IN PLOG_SCANXSACTION_CALLBACK | CallbackRoutine | The routine to call
for every unit of a transaction.
@parm IN PVOID | pContext | The context to be passed to state of the transaction.
@comm Stops enumerating the transaction units if the callback function returns
FALSE, or if the abort or commit record for this transaction is found or
if the next transacion is found.
@rdesc ERROR_SUCCESS if the state is found, else returns the error code.
@xref <f LogFindXsactionState>
****/
DWORD
LogScanXsaction(
IN HLOG hLog,
IN LSN StartXsactionLsn,
IN TRID XsactionId,
IN PLOG_SCANXSACTION_CALLBACK CallbackRoutine,
IN PVOID pContext)
{
PLOG pLog;
PLOGRECORD pRecord;
DWORD dwError = ERROR_SUCCESS;
int PageIndex, OldPageIndex;
RMID Resource;
TRID TrId;
TRTYPE TrType;
LSN Lsn;
PLOGPAGE pPage = NULL;
PUCHAR pLargeBuffer;
DWORD dwBytesRead;
RMTYPE ResourceFlags;
GETLOG(pLog, hLog);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogScanXsaction : Entry StartXLsn=0x%1!08lx! StartXId=%2!u!\r\n",
StartXsactionLsn, XsactionId);
Lsn = StartXsactionLsn;
if (Lsn >= pLog->NextLsn)
{
dwError = ERROR_INVALID_PARAMETER;
goto FnExit;
}
//read this record
dwBytesRead = 0;
if (LogRead( hLog, Lsn, &Resource, &ResourceFlags, &TrId, &TrType,
NULL, &dwBytesRead) == NULL_LSN)
{
dwError = GetLastError();
goto FnExit;
}
//check the record
if ((TrType != TTStartXsaction) ||
(TrId != XsactionId))
{
dwError = ERROR_INVALID_PARAMETER;
goto FnExit;
}
pPage = (PLOGPAGE)AlignAlloc(SECTOR_SIZE);
if (pPage == NULL) {
CL_UNEXPECTED_ERROR( ERROR_NOT_ENOUGH_MEMORY );
}
//initialize this to -1 so that the first page is always read
OldPageIndex = -1;
while (Lsn < pLog->NextLsn)
{
//
// Scan From Next record to find either the commit or abort record
//
PageIndex = LSNTOPAGE(Lsn);
if (PageIndex != OldPageIndex)
{
//read the page
pLog->Overlapped.Offset = PageIndex * pLog->SectorSize;
pLog->Overlapped.OffsetHigh = 0;
dwError = LogpRead(pLog, pPage, pLog->SectorSize, &dwBytesRead);
if (dwError)
{
goto FnExit;
}
//read was successful, no need to read the page unless the
//record falls on a different page
OldPageIndex = PageIndex;
}
pRecord = LSNTORECORD(pPage, Lsn);
//skip other log management records
if (pRecord->ResourceManager < RMAny)
{
Lsn = GETNEXTLSN(pRecord, TRUE);
continue;
}
//stop if next transaction record is encountered
if (pRecord->Transaction > XsactionId)
{
break;
}
//stop when a commit or abort record is found
if ((pRecord->Transaction == XsactionId) &&
((pRecord->XsactionType == TTCommitXsaction) ||
(pRecord->XsactionType == TTAbortXsaction)))
{
break;
}
//handle large records
if (pRecord->NumPages > 0)
{
//if the transaction id is the same
if ((pRecord->Transaction == XsactionId) &&
(pRecord->XsactionType == TTXsactionUnit))
{
//read the whole record
//for a large record you need to read in the entire data
pLargeBuffer = AlignAlloc(pRecord->NumPages * SECTOR_SIZE);
if (pLargeBuffer == NULL)
{
dwError = ERROR_NOT_ENOUGH_MEMORY ;
CL_LOGFAILURE(ERROR_NOT_ENOUGH_MEMORY);
break;
}
//read the pages
pLog->Overlapped.Offset = PageIndex * pLog->SectorSize;
pLog->Overlapped.OffsetHigh = 0;
dwError = LogpRead(pLog, pLargeBuffer, pRecord->NumPages *
pLog->SectorSize, &dwBytesRead);
//if it is the last page, then set the new page as the active
//page
if (dwError != ERROR_SUCCESS)
{
CL_LOGFAILURE(dwError);
AlignFree(pLargeBuffer);
break;
}
pRecord = LSNTORECORD((PLOGPAGE)pLargeBuffer, Lsn);
ClRtlLogPrint(LOG_NOISE,
"[LM] LogScanXsaction::Calling the scancb for Lsn=0x%1!08lx! Trid=%2!u! RecordSize=%3!u!\r\n",
Lsn, pRecord->Transaction, pRecord->DataSize);
//if the callback requests to stop scan
if (!(*CallbackRoutine)(pContext, Lsn, pRecord->ResourceManager,
pRecord->Flags, pRecord->Transaction,
pRecord->Data, pRecord->DataSize))
{
AlignFree(pLargeBuffer);
break;
}
}
//read the last page of the large record and advance
Lsn = GETNEXTLSN(pRecord,TRUE);
AlignFree(pLargeBuffer);
PageIndex = LSNTOPAGE(Lsn);
//read the page
pLog->Overlapped.Offset = PageIndex * pLog->SectorSize;
pLog->Overlapped.OffsetHigh = 0;
dwError = LogpRead(pLog, pPage, pLog->SectorSize, &dwBytesRead);
if (dwError)
{
goto FnExit;
}
OldPageIndex = PageIndex;
pRecord = (PLOGRECORD)((ULONG_PTR) pPage +
(Lsn - (pLog->Overlapped).Offset));
CL_ASSERT(pRecord->ResourceManager == RMPageEnd);
//move to the next page
Lsn = GETNEXTLSN(pRecord, TRUE);
}
else
{
if ((pRecord->Transaction == XsactionId) &&
(pRecord->XsactionType == TTXsactionUnit))
{
ClRtlLogPrint(LOG_NOISE,
"[LM] LogScanXsaction: Calling the scancb for Lsn=0x%1!08lx! Trid=%2!u! RecordSize=%3!u!\r\n",
Lsn, pRecord->Transaction, pRecord->DataSize);
//call the callback
if (!(*CallbackRoutine)(pContext, Lsn, pRecord->ResourceManager,
pRecord->Flags, pRecord->Transaction,
pRecord->Data, pRecord->DataSize))
{
break;
}
}
Lsn = GETNEXTLSN(pRecord, TRUE);
}
}
FnExit:
if (pPage) AlignFree(pPage);
return(dwError);
}