|
|||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Chapter 9 - Compensating Resource ManagersFrustrated with the inadequacies of the prevailing hierarchical storage schemes of the day, in 1970 an IBM researcher named Edgar Codd published a general specification for a relational database (Codd, Edgar. 1970. A Relational Model of Data for Large Shared Data Banks. Communications of the ACM, Vol. 13, No. 6, June, 377-387.). Codd did not say much about how his relational calculus might be implemented in a server, but he fired the starting gun for a new era in data storage technology. Companies like Oracle, Ingres, and of course IBM rushed to provide actual, scalable implementations of Codd’s work. After a period of contention about the query language (Oracle and others favored SQL—Ingres held out for something called QUEL), relational database technology became, more or less, industry standard. Relational database systems have grown significantly in terms of their capabilities. Far from simply being efficient, convenient repositories for data, they have also become safe, robust places to put data. Although each Database Management System (DBMS) handles transactions and disaster recovery differently, they all promise a high degree of fault-tolerance. For all that relational databases offer, however, there are simply times when you need to manipulate data that just doesn’t fit well into a relational model. And although it is a relatively straightforward process to write code to query from and modify data in some proprietary form, it is exponentially more complex to make the process fail-safe. Fortunately, COM+ provides a framework for writing server-side components that can modify data at the behest of a transactional client (and be governed by the Distributed Transaction Coordinator [DTC]), just as Microsoft SQL Server does. This framework is called the Compensating Resource Manager (CRM) and is the subject of this chapter. The Resource ManagerReinventing the code necessary to handle power-outages, user-requested rollbacks, interrupted modifications, and so on for non-relational data is not something most developers have the time and resources to do. Fortunately, COM+ provides an architecture that helps—that of the CRM. Before we talk about the CRM, we need to introduce some new terminology. Microsoft’s technology can sometimes be confusing because they often attach new names to existing technologies. For example, the terms OCX, ActiveX, OLE, and COM can all be used to describe the exact same technology. Usually, Microsoft reinvents a technology when it is refitted to play a part in some new, larger initiative. For example, the term OCX was once used to describe a graphical COM control that could be put on a VB form. When Microsoft took on Java on the Web-front, however, it adapted the concept of the OCX so that it could exist on an Internet Explorer 3 Web page. At this point, the humble OCX became an ActiveX control even though it didn’t fundamentally change. The term ActiveX became a more general term that described Microsoft’s overall Web strategy in a more generic fashion. According to the party line, the ActiveX control was a particular implementation and utilization of ActiveX technology. Just as Microsoft’s sudden emphasis on the World Wide Web warped established terminology and made strange bedfellows of previously unrelated or loosely related technologies, now COM+’s emphasis on the Enterprise is doing the same thing. In the new Enterprise philosophy, the term database (or worse, relational database) is thought to be too limiting. These words evoke images of SQL and relational tables, but not all corporate data repositories are based on relational databases. And so, Microsoft has adopted a term already used in the industry that can refer to any type of application that manages data and can participate in COM+ transactions—Resource Manager (RM). Strictly speaking, an RM must support COM+ transactions, but this is really to say that it can talk to the DTC and knows how to handle a two-phased commit. Specifically, an RM must support one or both of the following two-phased commit protocols:
Most major relational databases support one or both of these protocols. If they support OLE Transactions, they can automatically participate in COM+ transactions. If they only support XA, they can only participate in COM+ transactions if the ODBC driver (or some other form of resource dispenser the vendor makes available) works with the DTC to translate the OLE Transaction calls to XA-compliant ones. If the RM supports both, it gets the best of all worlds. So, RMs are commonly relational databases, but do not need to be. Microsoft Message Queue (MSMQ) is an example of an RM that is not a relational database. As you will see, COM+ messaging relies on MSMQ. It gives you the ability to send asynchronous transactional messages. We’ll discuss this more in Chapter 10, "Queued Components." XA and the DTCXA is not natively handled by the DTC. The DTC uses the OLE Transactions protocol (described in the next section, "Components of the CRM"). It is important to realize that although the DTC has facilities (a series of conversion interfaces) to convert an OLE-based transaction to an XA-based one, this is not an automatic process. The DTC cannot, on its own, interoperate with an XA-based RM like an older version of Oracle. However, if the ODBC driver (or other connection resource dispenser) supplied by the RM’s vendor is specifically designed to work with the DTC to perform this conversion behind the scenes, it may seem as though the DTC does automatically govern transactions on XA-only RMs. Don’t be fooled—the DTC is biased toward OLE Transactions. To work with XA-only RMs, the vendor has to supply a translator—usually buried in its ODBC driver or OLE-DB Provider. By supporting either XA or OLE Transactions, an RM is indicating it knows how to temporarily buffer changes to its data source, whatever that may be, and roll them back or commit them whenever asked by the DTC. This implies that the RM needs to keep some kind of log that records all changes. If the RM crashes, it can reopen the log when it is run again and either complete or undo whatever changes are necessary to ensure the integrity of its data. Furthermore, the RM must serialize all database changes while in the midst of a transaction. It would be very difficult to roll back changes indicated in the log, if other processes were continually allowed to change data underneath it. Most relational databases lock the table(s) or row(s) when a transaction is in progress and block all data modifications for all connections other than those enlisted in the transaction. The locks are released only after the transaction is complete. In short, writing a proper RM is difficult. The CRM is a kind of template and protocol that, if followed, takes a lot of the complexity out of writing an RM. Components of the CRMYou have data that is non-relational in nature and have decided to write your own CRM to manage this data on behalf of transactional COM+ objects. Although writing a true RM poses quite a challenge, writing a CRM is not difficult. A CRM consists of two separate COM coclasses that you must
implement. The first coclass is the WorkerThe Listing 9.1 Constants Common to Both Worker and Compensator 'Constants shared by both the compensator and worker components:
'Error Code indicating that the compensator is in recovery mode:
Public Const XACT_E_RECOVERYINPROGRESS = &H8004D082
'Constants used when writing the log file:
Public Const ADD_ACCOUNT_COMMAND = 1
Public Const REMOVE_ACCOUNT_COMMAND = 2
Public Const CHANGE_ACCOUNT_COMMAND = 3
The The relationship between the It is important to realize that, just as with our office example, a The CRM ClerkNote that we have introduced
another player in our drama—the The Dim Clerk as CRMClerk
Set Clerk = New CRMClerk
The Dim CrmLogControl As ICrmLogControl
Set CrmLogControl = Clerk
Note that in VB, the After the Listing 9.2 Writing an Array of Variants to the Log File Dim vntLogFileEntries(2) As Variant
vntLogFileEntries(0) = "LEDGERID:66:MAKEBALANCE:4500"
vntLogFileEntries(1) = "LEDGERID:176:MAKEBALANCE:5500"
vntLogFileEntries(2) = "WRITEEVENT: Debit and Credit for $500 complete"
On Error GoTo ErrorHandler
'Write to the log file, and ensure it is durable:CrmLogControl.WriteLogRecordVariants LogFileEntries
It really is as simple as it seems. The
Whatever logging protocol you create and use, it must be understood
by both the Let’s take a moment and summarize what we have so far. We know that
the The first example most
developers see of a CRM is one that creates and modifies flat files. In this CRM,
the Listing 9.3 A Complete CRM Worker Component that Modifies Account Balance in an XML File 'XMLWorker: Worker component for the XML CRM.
'
'This component exposes the interface that a client sees and interacts with.
'It is responsible for associating itself with a compensator component that
'understands the log it writes, and can commit/abort the operations that it
'performs
Option Explicit
'The worker component needs an instance of the CRM clerk, to associate itself
'with a compensator and to write its operations to the log:
Dim Clerk As CRMClerk
Dim CrmLogControl As ICrmLogControl
'The progID of the compensator this worker component is associated with:
Const COMPENSATOR_ProgID = "XMLCRMVB.XMLCompensatorVB"
Const COMPENSATOR_DESCRIPTION = "XML VB Compensator component"
'Indicates if the compensator has been registered:
Dim bIsCompensatorRegistered As Boolean
'A Variant array that is used to write to the log:
Dim vntLogFileEntries(3) As Variant
'This Worker component uses Microsoft’s XML Document
'Object Model (DOM) components to perform XML manipulation:
Dim xmlDoc As New DOMDocument
Dim xmlNodes As IXMLDOMNodeList
Dim xmlNode As IXMLDOMNode
Dim xmlBalanceNode As IXMLDOMNode
Dim xmlAccountNode As IXMLDOMNode
Dim xmlAccountBalanceNode As IXMLDOMNode
Dim xmlAccountNumberNode As IXMLDOMNode
Dim xmlRootNode As IXMLDOMNode
Private Sub Class_Initialize()
bIsCompensatorRegistered = 0
End Sub
'This routine is called by the Initialize method. ; It
'obtains an instance of the Clerk for writing to the log.
Private Sub ObtainCRMClerk()
On Error GoTo ErrorHandler
Set Clerk = New CRMClerk
Set CrmLogControl = Clerk
Exit Sub
ErrorHandler:
MsgBox "ObtainCRMClerk ErrorHandler " + Err.Description
End Sub
'This procedure associates this worker component with
'the compensator:
Private Sub RegisterCompensator()
'If we have already registered the compensator then
'exit the subroutine:
If bIsCompensatorRegistered = True Then
Exit Sub
End If
'Register the componensator with the DTC.
'There is the possibility that the Compensator is in the
'the process of recovering from a previous shutdown, hence
'the error checking loop:
On Error Resume Next
Do
CrmLogControl.RegisterCompensator COMPENSATOR_ProgID, ÂCOMPENSATOR_DESCRIPTION, CRMREGFLAG_ALLPHASES
DoEvents
Loop Until Err.Number < > XACT_E_RECOVERYINPROGRESS
'Was there an error registering the compensator?
If Err.Number <> 0 Then GoTo ErrorHandler
'Indicate that the compensator has been registered, so
'subsequent calls to register the compensator abort:
bIsCompensatorRegistered = True
Exit Sub
ErrorHandler:
MsgBox "XMLVBWorker: RegisterCompensator" & Err.Description & " - " & ; ; ; ;ÂHex$(Err.Number)
End Sub
'The following methods are exposed by the worker component to
'client applications. They perform the desired XML operations
'on the specified file, and write to the system log
'indicating what operations were performed. Note that each of these
'methods calls Initialize(), which registers the XML compensator
'if it has not already been registered:
Sub AddAccount(AccountNumber As Long, Balance As Double, Filename As String)
Dim dAccountBalance As Double
Dim iNode As Integer
On Error GoTo ErrorHandler
Initialize
'Check to see if the account already exists. We do this BEFORE
'writing to the log, because if it does exist we don’t want
'add an already existing account. Any easy way to check for existence
'is to run the GetBalance function against the account. If there IS
'a balance, then the account already exists so we have a problem:
If GetBalance(AccountNumber, Filename, dAccountBalance) Then
MsgBox "Account already exists"
GetObjectContext.SetAbort
Exit Sub
End If
'Before we perform ANY operations on the XML file,
'we have to write to the log to indicate what we are doing.
'Our entry in the log will contain:
' 1.) The operation (Adding an Account)
' 2.) The AccountNumber we are adding
' 3.) The Balance it will start with
' 4.) The XML filename we are writing to:
vntLogFileEntries(0) = ADD_ACCOUNT_COMMAND
vntLogFileEntries(1) = AccountNumber
vntLogFileEntries(2) = Balance
vntLogFileEntries(3) = Filename
'Write to the log file, and ensure it is durable:
CrmLogControl.WriteLogRecordVariants vntLogFileEntries
CrmLogControl.ForceLog
'Log file was written, so now try to add the account to the
'XML file. This uses Microsoft’s XML Parser:
xmlDoc.Load Filename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
'Check to make sure we are not adding an account that
'already exists. We already did this, but make sure
'since we don’t have an exclusive lock on the file:
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = AccountNumber Then
GetObjectContext.SetAbort
Exit Sub
End If
Next
'The account does not exist, so we can add it without any problems:
Set xmlRootNode = xmlDoc.documentElement
Set xmlAccountNode = xmlDoc.createElement("ACCOUNT")
Set xmlAccountNumberNode = xmlDoc.createElement("ACCOUNTNUMBER")
Set xmlAccountBalanceNode = xmlDoc.createElement("BALANCE")
xmlAccountBalanceNode.Text = Balance
xmlAccountNumberNode.Text = AccountNumber
xmlAccountNode.appendChild xmlAccountNumberNode
xmlAccountNode.appendChild xmlAccountBalanceNode
xmlRootNode.appendChild xmlAccountNode
xmlDoc.save Filename
GetObjectContext.SetComplete
Exit Sub
ErrorHandler:
MsgBox "An error occured in ADD " & Err.Description
End Sub
Sub RemoveAccount(AccountNumber As Long, Filename As String)
Dim dBalanceBefore As Double
Dim iNode As Integer
On Error GoTo RemoveAccountProblem
Initialize
'Since we are changing the account balance we need to obtain
'the CURRENT balance and write to the log file (in case the
'transaction is rolled back)
If Not GetBalance(AccountNumber, Filename, dBalanceBefore) Then
'There was a problem getting the current balance
'from the XML file, so abort the operation:
MsgBox "Couldn’t get balance!"
GetObjectContext.SetAbort
Exit Sub
End If
vntLogFileEntries(0) = REMOVE_ACCOUNT_COMMAND
vntLogFileEntries(1) = AccountNumber
vntLogFileEntries(2) = dBalanceBefore
vntLogFileEntries(3) = Filename
'Write to the log file, and ensure it is durable:
CrmLogControl.WriteLogRecordVariants vntLogFileEntries
CrmLogControl.ForceLog
xmlDoc.Load Filename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
'Check to make sure we are not adding an account that
'already exists:
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = AccountNumber Then
Exit For
End If
Next
'Is the account we wish to remove there?
If xmlNode.Text < > AccountNumber Then
GetObjectContext.SetAbort
Exit Sub
End If
'Remove it from the file:
xmlDoc.childNodes.Item(0).removeChild xmlNodes.Item(iNode)
xmlDoc.save Filename
GetObjectContext.SetComplete
Exit Sub
RemoveAccountProblem:
MsgBox "Account was NOT removed!"
End Sub
Sub CreditAccount(AccountNumber As Long, Amount As Double, Filename As String)
ChangeAccount AccountNumber, Amount, 1, Filename
End Sub
Sub DebitAccount(AccountNumber As Long, Amount As Double, Filename As String)
ChangeAccount AccountNumber, Amount, 0, Filename
End Sub
Private Sub ChangeAccount(AccountNumber As Long, Amount As Double, creditOrDebit ÂAs Boolean, Filename As String)
Dim dBalanceBefore As Double
On Error GoTo ChangeAccountProblem
Dim iNode As Integer
Initialize
'Since we are changing the account balance we need to obtain
'the CURRENT balance and write to the log file (in case this
'transaction is rolled back)
If Not GetBalance(AccountNumber, Filename, dBalanceBefore) Then
'There was a problem getting the current balance
'from the XML file, so abort the operation:
GetObjectContext.SetAbort
Exit Sub
End If
vntLogFileEntries(0) = CHANGE_ACCOUNT_COMMAND
vntLogFileEntries(1) = AccountNumber
vntLogFileEntries(2) = dBalanceBefore
vntLogFileEntries(3) = Filename
'Write to the log file, and ensure it is durable:
CrmLogControl.WriteLogRecordVariants vntLogFileEntries
CrmLogControl.ForceLog
On Error GoTo ChangeAccountProblem
xmlDoc.Load Filename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
'Check to make sure we are not adding an account that
'already exists:
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = AccountNumber Then
Exit For
End If
Next
'Is the account we wish to remove there?
If xmlNode.Text <> AccountNumber Then
GetObjectContext.SetAbort
Exit Sub
End If
Set xmlBalanceNode = xmlNodes.Item(iNode).childNodes.Item(1)
'Debit or Credit the Account:
If creditOrDebit Then
xmlBalanceNode.Text = xmlBalanceNode.Text + Amount
Else
xmlBalanceNode.Text = xmlBalanceNode.Text - Amount
End If
xmlDoc.save Filename
GetObjectContext.SetComplete
Exit Sub
ChangeAccountProblem:
GetObjectContext.SetAbort
End Sub
'The GetBalance function places the Balance of the desired account in the Balance
'variable. This function exists because in both the RemoveAccount and ÂChangeAccount
'routines we need the balance of the Account before any changes are made,
'so we can write it to the log file.
Private Function GetBalance(AccountNumber As Long, Filename, ByRef Balance As ÂDouble) As Boolean
Dim iNode As Integer
On Error GoTo GetBalanceProblem
xmlDoc.Load Filename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
'Find the Account Number we want:
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = AccountNumber Then
Exit For
End If
Next
'Is the account we wish to remove there?
If xmlNode.Text <> AccountNumber Then
GetBalance = False
Exit Function
End If
'Determine the Balance:
Set xmlBalanceNode = xmlNodes.Item(iNode).childNodes.Item(1)
Balance = xmlBalanceNode.Text
GetBalance = True
Exit Function
GetBalanceProblem:
GetBalance = False
End Function
'Initialize obtains the interface to access the log
'and registers the compensator component:
Private Sub Initialize()
'Have we obtained a ICrmLogControl interface yet?
'If not obtain it from the CRMClerk:
If CrmLogControl Is Nothing Then
ObtainCRMClerk
End If
'Register the compensator with the DTC:
Call RegisterCompensator
End Sub
CompensatorThe Compensator’s job is to either commit the
changes initiated by the COM+ objects call on the Worker, but COM+ itself calls on the services of the
Clerk.RegisterCompensator "MyCompensator.Comp", "[put your description here]", ÂCRMREGFLAG_ALLPHASES
The last argument is a flag that indicates which phases the Note that although a
COM+ automatically QIs the For simplicity, let’s assume that variants are sufficient to
represent our data modifications in the log. In this case, our The complete source code for
our XML Phase I: PrepareThis phase involves three method calls, described in section "Step
1: General Prepare." Before I discuss them fully, note that it is only possible
for a CRM to abort a transaction during this phase. One of the methods of the CRMClerk’s
default interface, The Although the 'remember, the Worker calls this method early in its life
'to associate itself with a specific class of Compensator
Clerk.RegisterCompensator "MyCompensator.Comp", "[put your description here]",
WhatToParticipateIn
These flags are made to be bit-wise OR’d, meaning they can be
combined where their effects are additive. There are no absolute rules as
to what CRM phases your Keep in mind that by not
participating in a phase (particularly the Assuming that the Step 1: General PrepareBeginPrepareVariants()
The first step in the two-phase commit involves COM+ calling the
CRM’s Step 2: Prepare to Make Changes for Each Log Entry PrepareRecordVariants( [in] VARIANT * pLogRecord,
[out, retval] VARIANT_BOOL * pbForget
);
This method is called once for
every log entry made in the CRMClerk by the The first argument contains the current log entry; so if this is
the fifth time COM+ has made this call, this argument contains the fifth log
entry made by the The second argument, pbForget, gives the It is important to realize that,
like Step 3: Finalize Changes EndPrepareVariants( [out, retval] VARIANT_BOOL * pbOkToPrepare);
When COM+ calls this method, it is notifying the CRM that it has
received all the log entries and the Do not tell COM+ that it is okay to prepare unless you really mean
it. By returning TRUE, you are promising COM+ that you are
confident that you can commit the transaction in the future. COM+ reserves the
right to reinstantiate and pester your Phase II: CommitThe You might experience a sense of déjà
vu when you look at these methods because some have the identical form
of their counterpart functions in the Step 1: Prepare to Commit BeginCommitVariants([in] VARIANT_BOOL * bRecovery, );
COM+ calls this method to notify the CRM that it should prepare to
commit what the The second argument has a value of TRUE if something
catastrophic happens after the Step 2: Make Changes for Each Log Entry CommitRecordsVariants( [in] VARIANT * pLogRecord,
[out, retval] VARIANT_BOOL * pbForget
);
This method has the same form and purpose as its counterpart Step 3: Commit Changes EndCommitVariants()
This method notifies the Aborting TransactionsSadly, we acknowledge that not
every transaction can commit. Even if all COM+ objects vote Yes on the
transaction and COM+ attempts to commit it via the DTC, any participating RM or
CRM can fail. The RM or In the event of a rollback request by the Distributed Transition Coordinator (DTC), COM+ calls the following methods of the CRM’s ICrmCompensatorVariants (or ICrmCompensator if using binary data) interface. However, these methods are only called if the transaction aborts in the following manners:
Also note that the CRM abort methods are not called if the CRM
crashes during the Step 1: Prepare to Abort BeginAbortVariants [in] VARIANT_BOOL * recovery)
This method notifies the The recovery argument is TRUE if the Compensator crashes
during or after its Step 2: Undo Each Log Entry AbortRecordVariants ([in] VARIENT * pLogRecord,
[out, retval] VARIANT_BOOL * pbForget)
This method is called once for
every record in the log. Note that the entries are in reverse order. The Step 3: Finalizing Abortion EndAbortVariants()
Handling RecoveryWe have discussed that the successful completion of the There is no simple answer. Ideally, you design your There is one final important point I want to make about recovery—that of idempotence. You won’t find the term idempotence in the dictionary, but you will hear it bantered about in mathematical and development circles. An action is said to be idempotent ifit can occur any number of times, but the frequency does not affect the result. For example, if you hit the elevator Up button in the lobby, get impatient, and keep hitting it, your action is idempotent—no matter how many times you hit the button, the elevator does not move any faster and the ultimate outcome is the same. In the event of crash recovery, it is possible that your When In DoubtIn the process of a two-phase commit, there is a period of time
between the Thus, an RM might get a prepare
notification from its DTC, complete it, but wait for some time while other
participating RMs are completing their Prepare phases. The prepared RM has said
it is ready to commit, but the possibility still exists that another RM might
fail its Doubt should be fleeting and
probably last no more than a fraction of a second. However, if an RM crashes
during the in-doubt state, the DTC records that the whole distributed
transaction becomes in doubt. When the failed RM is next run, it contacts the
DTC, asks about the status of the transaction, and both parties try to
reconcile the transaction. If the other RMs complete their Prepare phase and
the controlling DTC issues a commit command, the recovering RM is on the hook
to commit its trans-action. If, after reawakening, the failed RM discovers that
another RM has not completed its The Complete CompensatorNow that we’ve stepped through
the phases of Listing 9.4 CRM Compensator Component that Commits or Aborts Changes to an XML File Made by the Worker 'XMLCompensator: Compensator component for the XML CRM.
'
'This component is called when a transaction involving the XMLWorkerVB component
'aborts or commits. Either one of two interfaces MUST be implemented by all
'compensator components:
'ICrmCompensator — for unstructured storage in the log
' (C++ components)
'ICrmCompensatorVariants — for structured storage in the log
' (VB/Java):
Option Explicit
'The worker component this compensator is associated with writes
'variants to the log, so this component implements the ICrmCompensatorVariants
'interface:
Implements ICrmCompensatorVariants
'The DTC gives the ICrmLogControl interface to the compensator
'in the SetLogControlVariants method so the compensator can access
'the log:
Dim CrmLogControl As ICrmLogControl
'Like the worker, this compensator uses Microsoft’s XML Document
'Object Model (DOM) components to perform XML manipulation:
Dim xmlDoc As New DOMDocument
Dim xmlNodes As IXMLDOMNodeList
Dim xmlNode As IXMLDOMNode
Dim xmlAccountNode As IXMLDOMNode
Dim xmlAccountBalanceNode As IXMLDOMNode
Dim xmlAccountNumberNode As IXMLDOMNode
Dim xmlRootNode As IXMLDOMNode
'Variables to read from the log file:
Dim vntCommand, vntAccountNumber, vntBalanceBefore, vntFilename
'AbortRecordVariants is called after BeginAbortVariants.
'This method delivers log records that were written by the
'worker. The compensator must "undo" the changes indicated
'by such records.
Private Function ICrmCompensatorVariants_AbortRecordVariants(pLogRecord As ÂVariant) As Boolean
Dim iNode As Integer
'Obtain information from the logfile that indicates what was done:
vntCommand = pLogRecord(0)
vntAccountNumber = pLogRecord(1)
vntBalanceBefore = pLogRecord(2)
vntFilename = pLogRecord(3)
On Error GoTo AbortRecordVariantProblem
'Determine what operation was performed and "undo" it:
If vntCommand = ADD_ACCOUNT_COMMAND Then
'The log indicates that an account was ADDED, and so we must remove it:
xmlDoc.Load vntFilename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = vntAccountNumber Then
Exit For
End If
Next
'Is the account we wish to remove there?
If xmlNode.Text <> vntAccountNumber Then
'The account is already gone, so we are ok.
Exit Function
End If
'Remove the account from the XML file:
xmlDoc.childNodes.Item(0).removeChild xmlNodes.Item(iNode)
xmlDoc.save vntFilename
ElseIf Command = REMOVE_ACCOUNT_COMMAND Then
'An account was removed. To undo this action we must
'add the account with its previous balance. A true
'rollback scenario would add the account in its proper
'place in the XML file. For our purposes, we just
'add the account to the end of the XML file.
xmlDoc.Load vntFilename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
'Check to make sure we are not adding an account that
'already exists.
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = vntAccountNumber Then
ICrmCompensatorVariants_AbortRecordVariants = True
Exit Function
End If
Next
Set xmlRootNode = xmlDoc.documentElement
Set xmlAccountNode = xmlDoc.createElement("ACCOUNT")
Set xmlAccountNumberNode = xmlDoc.createElement("ACCOUNTNUMBER")
Set xmlAccountBalanceNode = xmlDoc.createElement("BALANCE")
xmlAccountBalanceNode.Text = vntBalanceBefore
xmlAccountNumberNode.Text = vntAccountNumber
xmlAccountNode.appendChild xmlAccountNumberNode
xmlAccountNode.appendChild xmlAccountBalanceNode
xmlRootNode.appendChild xmlAccountNode
xmlDoc.save vntFilename
ElseIf vntCommand = CHANGE_ACCOUNT_COMMAND Then
'The account was debited/credited. To undo this we
'just have to restore the previous balance:
xmlDoc.Load vntFilename
Set xmlNodes = xmlDoc.childNodes.Item(0).childNodes
For iNode = 0 To xmlNodes.length - 1
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(0)
If xmlNode.Text = vntAccountNumber Then
Exit For
End If
Next
If xmlNode.Text <> vntAccountNumber Then
ICrmCompensatorVariants_AbortRecordVariants = True
End If
Set xmlNode = xmlNodes.Item(iNode).childNodes.Item(1)
xmlNode.Text = vntBalanceBefore
xmlDoc.save vntFilename
End If
ICrmCompensatorVariants_AbortRecordVariants = True
Exit Function
AbortRecordVariantProblem:
MsgBox "XMLWorker, AbortRecordVariant error: " & Err.Description
End Function
'BeginAbortVariants is called to let the compensator know that it
'must abort the current transaction. A call to this method is followed
'by a call to AbortRecordVariants, where the compensator recieves
'log records of the operations it must undo. In this phase
'the compensator will do any preparatory work it has to, to undo these
'changes (this compensator does not have any such preparatory work)
Private Sub ICrmCompensatorVariants_BeginAbortVariants(ByVal bRecovery As Boolean)
End Sub
'BeginCommitVariants is called to let the compensator know that the
'second phase of the two phase commit has been reached. A call to this
'method is followed by a call to CommitRecordVariants, where the compensator
'receives log records of the operations is must commit. In this phase
'the compensator will do any preparatory work it has to, to commit these changes
'(this compensator does not have any such preparatory work)
Private Sub ICrmCompensatorVariants_BeginCommitVariants(ByVal bRecovery As ÂBoolean)
End Sub
'BeginPrepareVariants is called to let the compensator know that the
'first phase of the two phase commit has been reached. A call to this
'method is followed by a call to PrepareRecordVariants, where the compensator
'receives log records of the operations is must prepare. In this phase
'the compensator will do any preparatory work it has to, to prepare these changes
'(again, this compensator does not have any such preparatory work)
Private Sub ICrmCompensatorVariants_BeginPrepareVariants()
End Sub
'CommitRecordVariants is called after BeginCommitVariants.
'This method delivers log records that were written by the
'worker. The compensator must do whatever it has to do
'to make the changes specified in the log records permanent.
'In this example, since the XML operations have already
'been performed by the worker, the compensator doesn’t
'have to do anything to make them permanent (the only work
'the compensator has to do is during the abort phase — see
'AbortRecordVariants).
Private Function ICrmCompensatorVariants_CommitRecordVariants(pLogRecord As ÂVariant) As Boolean
ICrmCompensatorVariants_CommitRecordVariants = False
End Function
'EndAbortVariants is called at the end of the abort phase, after
'AbortRecordVariants.This is the last method called during the
'abort phase. This is where the compensator
'would do any cleanup operations associated with its entire abort phase.
Private Sub ICrmCompensatorVariants_EndAbortVariants()
End Sub
'EndAbortVariants is called at the end of the commit phase, after
'ComitRecordVariants. This is the last method called during the
'commit phase. This is where the compensator would do any cleanup
'operations associated with its entire commit phase.
Private Sub ICrmCompensatorVariants_EndCommitVariants()
End Sub
'EndPrepareVariants is called at the end of the prepare phase, after
'PrepareRecordVariants. This is the last method called during the
'prepare phase. This is where the compensator would do any cleanup
'operations associated with its entire prepare phase. In addition,
'this compensator returns a boolean in this method: if the prepare
' phase proceeded smoothly, the compensator indicates it is ready for
'the commit phase by returning true. If there was a problem during the
'prepare phase, the compensator returns false, at which point the
'transaction is aborted.
Private Function ICrmCompensatorVariants_EndPrepareVariants() As Boolean
ICrmCompensatorVariants_EndPrepareVariants = True
End Function
'PrepareRecordVariants is called after BeginPrepareVariants.
'This method delivers log records that were written by the
'worker. The compensator must do whatever it has to do
'to prepare the records for the commit phase of the transaction.
Private Function ICrmCompensatorVariants_PrepareRecordVariants(pLogRecord As ÂVariant) As Boolean
End Function
'SetLogControlVariants is called by the DTC to give the compensator
'an instance of ILogControl so it can access the log:
Private Sub ICrmCompensatorVariants_SetLogControlVariants(ByVal pLogControl As COMSVCSLib.ICrmLogControl)
Set CrmLogControl = pLogControl
End Sub
Listing 9.5: XML Wrapper Component (This Allows Clients to Explicitly Abort or Commit the Transactions of The Worker Component): 'XMLCRMWrapper.
'This component "wraps" the four methods of the XML CRM Component
'(AddAccount, RemoveAccount CreditAccount, DebitAccount). In addition
'to wrapping these methods, it exposes two methods called
'Abort and Commit, which Abort and Commit the transaction by calling
'SetAbort and SetComplete.
'Wrapper components give a client the ability
'to explicitly abort or commit a transaction. Alternatively, a client
'could use the TransactionContext object to accomplish the same thing.
Option Explicit
'The following error code results when one tries to execute a method call
'on a COM+ component that has a transaction and has already aborted or
'in the process of aborting that transaction. See ErrorHandler for details
Const CONTEXT_E_ABORTING = &H8004E003
Dim pObjectContext As ObjectContext
Dim pObjectState | ||||||||||||||||||||||||||||||||||||