Applying COM+ - Chapter 9: Compensating Resource Managers






4.67/5 (6 votes)
Mar 20, 2001

69327
COM+ holds the promise of greater efficiency and more diverse capabilities for developers who are creating applications - either enterprise or commercial software - to run on a Windows 2000 system.
![]() |
|
Chapter 9 - Compensating Resource Managers
Frustrated 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 Manager
Reinventing 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:
- X/Open XA. X/Open (www.opengroup.com) is an international standards body that defined the XA standard. XA is not an acronym, rather it is the name of a protocol. Specifically, it is a two-phased commit protocol originating in the UNIX world. Most popular transaction monitors (TMs) systems—BEA Tuxedo and Encina, to name a couple—interoperate with a variety of relational databases such as Oracle, DB2, and Sybase (all are examples of RMs) to make distributed transactions possible. UNIX-style TMs perform the same functionality as Microsoft’s DTC, and XA is the protocol that the TMs use to govern one or many XA-compliant RMs during a distributed transaction.
- OLE Transactions. This protocol is a two-phased commit protocol just like XA. OLE Transactions is based on methods callable from COM interfaces; XA, on the other hand, is API-based. In the end, however, both protocols accomplish the same result. OLE Transactions can be said to be DTC’s native two-phase protocol. For the DTC (and therefore COM+ transactions) to work with an RM, the RM must support OLE Transactions or must supply an OLE Transaction-to-XA conversion mechanism. As discussed in "XA and the DTC," sidebar, this conversion mechanism is usually located in the ODBC driver or OLE-DB provider supplied by the vendor.
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 DTC
XA 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 CRM
You 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 Worker
.
The second coclass is the Compensator
.
Worker
The Worker
coclass
supports whatever methods you choose to give it. In our example (Listing 9.1),
we have a Worker
class with methods like AddAccount()
, ChangeAccount()
,
and so on. In the transactional examples we’ve discussed so far, COM+ objects
are manipulating relational databases (which are RMs) via ADO methods like Execute()
.
Philosophically, an object that calls ChangeAccount()
on our Worker
object is doing the exact same thing as an object
executing an update statement via some method of ADO; it is requesting that a
data change be made by an underlying RM in the context of a transaction.
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 Worker
knows
about the structure of the underlying data and knows how and where to make
changes. However, and this is the interesting part, although the Worker
coclass might make changes to the underlying data, these changes require the
participation of the Compensator
to make these changes permanent or to undo them
altogether should any COM+ object participating in the transaction call SetAbort()
.
The relationship between the Worker
and Compensator
is
very much like the relationship
between an office worker and his office manager. It is often, though not
necessarily, the case that a worker (Worker
) does most of the legwork, but ultimately it is
the manager (Compensator
) who applies a rubber stamp to make the change
permanent and official. On the other hand, the manager might say, "This work is
no good!" or "One of the higher-ups have cancelled the project, so what you’ve
done is no longer valid" and see to it that all the work is undone. Of course,
different offices have different balances of responsibility between workers and
their managers. In some companies, the clerk might merely propose a thing, and
the manager actually does most the legwork to make it happen. There is no
absolute rule about who does what, except that both the Worker
and Compensator
must work together.
It is important to realize that, just as with our office example, a
Worker
and Compensator
are not necessarily working at the same time. It might be the case that the Worker
works the evening shift and leaves some kind of information log about what he
has done with an office clerk. When the manager (Compensator
) comes in
the next morning, the Clerk
gives the manager the log and says, "This
what the Worker proposed doing, can you approve this and make it
permanent?"
The CRM Clerk
Note that we have introduced
another player in our drama—the Clerk
. The Clerk
keeps log information on behalf of the Worker and
shares it with the Compensator
when the Compensator
is available. In this metaphor, there are three
roles or characters—that of Worker
, Compensator
, and Clerk
. I have chosen these characters because they
have direct parallels when writing a real CRM.
The Worker
object must support interfaces and
methods that transactional COM+ objects can call to make modifications to your
data. In a very real sense, the Worker
is your RM, so these methods can be whatever you want them to be and can do
whatever you deem appropriate. Whatever these methods do, however, it is
critical that the Worker
record any actions resulting from each
method call to some kind of log before the Worker
makes any kind of physical change. Remember, just as
with our office example, there is a clerk who records a log of what the Worker
intends to do and passes it from Worker
to Compensator
. It is
ultimately the Compensator
who either approves the changes and
makes them permanent or undoes them. If the Worker
fails to record
his actions before doing them and then decides to quit (or crash), the Compensator
has no idea how to undo the Worker’s changes. This is why it is
important that the Worker
log his actions with the Clerk
before making any physical changes. In the CRM, this clerk is called,
appropriately, the CRMClerk.
The CRMClerk is a COM object defined in the comsvcs.dll type
library that all Workers must ask for and use. It creates a log
on behalf of the Worker
; after the Worker
obtains an
instance of the CRMClerk
, it can call the Clerk’s
methods to make additions to this log. Getting the Clerk
is as simple as
using the following code:
Dim Clerk as CRMClerk
Set Clerk = New CRMClerk
The Clerk
allows the Worker
to write to a
log file (created and maintained by Clerk
) by supporting the ICrmLogControl
interface. The Worker
need only QueryInterface (QI)
for it, as demonstrated:
Dim CrmLogControl As ICrmLogControl
Set CrmLogControl = Clerk
Note that in VB, the ICrmLogControl
interface is the CRMClerk’s
default interface. You can either call ICrmLogControl’s methods through a
variable of type ICrmLogControl
or directly on the Clerk
object itself.
After the Worker
has the ICrmLogControl
interface, it can write to the Clerk’s log file. The Worker
can either write an array of variants or a binary large object (BLOB). Let’s
demonstrate using variants (see Listing 9.2).
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 Worker
writes
arrays of variants via the CRMClerk
to the log file the CRMClerk
will maintain (a complete CRM Worker is provided in Listing 9.3). It is
completely up to the developer to decide how much and what kind of data should
be logged. Ultimately, the log file is presented to the Compensator
in
one of the following two ways:
- If all COM+ objects participating in the
transaction vote that the transaction is good, the
Compensator
is told to commit the changes and is given the log. - If one or more objects vote for the
transaction to fail, however, the
Compensator
is asked to roll back the changes and once again, is given the log to do so.
Whatever logging protocol you create and use, it must be understood
by both the Worker
and Compensator
. What’s more, the log should
contain all the information that the Compensator
could possibly need to either
commit or roll back the changes.
Let’s take a moment and summarize what we have so far. We know that
the Worker
writes data changes to a log file, and we know that that log file will be given
to the Compensator
and asked by COM+ to either commit or roll back the changes. This question,
however, arises: Who does the physical work of modifying the data, the Worker
or the Compensator
?
Writing to a log file is one thing, and it is clear that the Worker
writes to the log file via the CRMClerk
, and the Compensator is
given the log when the COM+ transaction as a whole commits or aborts. When it
comes to actual, physical changes, the developer determines how much actual
work the Worker
and Compensator
each perform. It is really
governed by what kind of data the CRM is supposed to modify and how many steps
are necessary to modify it.
The first example most
developers see of a CRM is one that creates and modifies flat files. In this CRM,
the Worker
creates the file in a temporary directory after writing the file’s
path in the log. The Compensator
then receives the log and from it, figures out
where the temporary file is. If the Compensator
is called in the context of a commit, it copies
the file to the permanent directory. If it is called because a participating
object named SetAbort()
, it deletes the file from the temporary
directory. Without getting too complex but adding a little spice, Listing 9.3
displays the code for a CRM Worker
that performs modifications to an XML file
using Microsoft’s XML Document Object Model (DOM). If the listing seems
daunting, just read the source comments along the way and you’ll find that
there is nothing at all complicated about a CRM Worker.
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
Compensator
The Compensator’s job is to either commit the
changes initiated by the Worker
or roll them back. COM+ notifies the Compensator
of which action to perform, commit or roll back, and presents the Compensator
with each and every entry in the log, one at a time. Interestingly, if the Compensator
is asked to commit, it is given each log entry in the same order as it was
written by the Worker. If, on the other hand, the Compensator
is asked to roll back, it is given each log entry in reverse order. We look at
this process in the next couple paragraphs.
COM+ objects call on the Worker, but COM+ itself calls on the services of the
Compensator
.
A given Compensator
is associated with a particular Worker
by virtue of the Worker
executing the following code early in its
life:
Clerk.RegisterCompensator "MyCompensator.Comp", "[put your description here]", ÂCRMREGFLAG_ALLPHASES
The last argument is a flag that indicates which phases the Compensator
wants to be involved in. It can choose to be called during the Commit
phase and
not in the Prepare
phase, if there is nothing it needs to do in the latter and
thus wants to improve performance. For now, we’re going to assume that our Compensator
wants to be involved in all phases as this is the norm. For completeness,
however, a description of each registration flag can be found in the subsequent
section, "Phase I: Prepare."
Note that although a Worker
explicitly associates itself with
a particular class of Compensator
, they are not necessarily running at
the same time. And under no circumstances does the Worker
call methods on
the Compensator
,
or vice versa. Their only form of communication, by design, is the log. Upon
instantiating the Compensator
, COM+ gives it an ICrmLogControl
interface however, the Compensator
cannot use this interface to read
the log, only to write to it. To read the changes made by the Worker
,
a Compensator
must implement one of the following two interfaces:
- ICrmCompensator
- ICrmCompensatorVariants
COM+ automatically QIs the Compensator
for one of the above
interfaces and uses its methods to "feed" the Worker’s changes to the Compensator
.
The particular inter-face your Compensator
should implement depends on
the type of data it works with. Specifically, if the Compensator
is
dealing with binary data, it implements the ICrmCompensator
if it
is dealing with data that can confidently be represented by variants, it uses ICrmCompensatorVariants
.
In either case, COM+ calls methods on these interfaces to inform the Compensator
what the specific changes are (that is, what the Worker
wrote to the
log) and whether it should commit them or roll them back.
For simplicity, let’s assume that variants are sufficient to
represent our data modifications in the log. In this case, our Compensator
only needs to implement the ICrmCompensatorVariants
interface.
The complete source code for
our XML Compensator
can be found under the section, "The Complete Compensator," found toward the end of this chapter. I am not
going to show its source code here because the Compensator has a more complicated job than the Worker, and
we need to lay a little more groundwork before its implementation will make
perfect sense. Remember that COM+ transactions occur in two distinct phases—a
Prepare
phase and a Commit
phase. If all objects participating in a transaction
vote positively, your CRM is asked to commit. For now, let’s assume this is the
case—all the objects have voted positively and COM+ wants to commit the
transaction. After instantiating the Compensator
, COM+ QIs for its ICrmCompensatorVariants interface. It then calls the methods of this
interface to walk your CRM through the Prepare
and Commit
phases. I’ll talk
about the former case first.
Phase I: Prepare
This 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, ICrmLogControl
, is ForceTransactionToAbort()
.
Because the Compensator
is given this interface on instantiation (COM+
calls its SetLogControlVariants
method), you might think that the Compensator
can call this method to abort the transaction it doesn’t work. This method is
for the Worker, and as you see, the only way a Compensator
can abort a transaction during the Prepare
phase is by returning FALSE
when COM+ calls its final preparation method.
The Prepare
phase is crucially important, because after a Compensator
indicates it has successfully completed this phase, the CRM can no longer abort
the transaction. COM+ expects the Compensator to be able to commit the
changes, no matter what else might happen.
Although the Prepare
phase is important, it is allowable for a Compensator
to opt out of this phase altogether. This is not to say that the Prepare phase
doesn’t happen; it is an inherent part of Object Linking and Embedding (OLE)
Transactions and always occurs when COM+ attempts to commit a transaction. The Compensator
does, however, have the option not to be notified of this phase or to participate
in it. It does this by specifying one or a combination of five enumerated
values in the third argument to ICrmLogControl’s RegisterCompensator()
method, as follows:
'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
WhatToParticipateIn
can be one of the following:
CRMREGFLAG_ALLPHASES
CRMREGFLAG_PREPAREPHASE
CRMREGFLAG_COMMITPHASE
CRMREGFLAG_ABORTPHASE
CRMREGFLAG_FAILIFINDOUBTSREMAIN
These flags are made to be bit-wise OR’d, meaning they can be
combined where their effects are additive. CRMREGFLAG_ALLPHASES
is just a combination of all the flags except for CRMREGFLAG_FAILIFINDOUBTSREMAIN
.
We will talk about this last flag in the subsequent section "When In Doubt,"
but the other flags are pretty straightforward. The word PHASE
does not refer to the two phases of OLE Transactions; they refer to the various
phases of interaction between the CRM and COM+.
There are no absolute rules as
to what CRM phases your Compensator
should participate in; only the developer can
make this judgment (the Compensator
example in Listing 9.4 participates in all of
them). They are included to provide the Compensator
developer with a means of improving performance
by eliminating unnecessary method calls. And, in case you might be thinking
it—no, you cannot register different Compensators
to handle different phases for the same Worker.
Keep in mind that by not
participating in a phase (particularly the Prepare
phase), your Compensator
is implicitly indicating its approval and is saying, in effect, "I wouldn’t do
anything if you did call me during this phase, and I would have given you the
okay anyway. So, don’t bother me, and proceed as if I said everything was
fine."
Assuming that the Worker
asked the Compensator
to
be involved in all phases(CRMREGFLAG_ALLPHASES
), COM+ undertakes the
following steps and calls the following methods during the Prepare
phase of a
commit.
Step 1: General Prepare
BeginPrepareVariants()
The first step in the two-phase commit involves COM+ calling the
CRM’s BeginPrepareVariants()
method. It takes no arguments, and COM+ makes no assumptions about what the Compensator
will do when this method is called. It is really just a notification. COM+
wants the Compensator
to do whatever it needs to do to prepare to
commit a transaction. Some Compensators
might not do anything;
others might allocate a new block of memory, disk space, and so on.
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 Worker
object. COM+ hands each entry to the
Compensator
,
one at a time. The Compensator
knows when it has received every entry, because
COM+ calls the EndPrepareVariants()
(discussed in the next section) function to
indicate when this is the case.
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 Worker
. Note that one entry can contain one or
many variant values. Basically, I am using the term entry to describe one call
made by the Worker
to WriteLogRecordVariants()
.
The second argument, pbForget, gives the Compensator
the opportunity to remove an item from the log. A Compensator
might want
to remove items from the log that are purely informational and relate only to
the Prepare phase but not the Commit phase. In other words, the Worker
might want to send the Compensator information about something it needs
to prepare for. This information might not, however, be pertinent after the
preparation is complete and the CRM is in the Commit
phase, so the Compensator
has an opportunity to remove it while still in the Prepare
phase. There are no
absolute rules; this is only one possibility. Don’t feel pressured to use this
(or any argument) just because it is there.
It is important to realize that,
like BeginPrepareVariants()
, the EndPrepareVariants()
method is informational. What the
Compensator
does or doesn’t do when this method is called is up to it. COM+ just wants to be
sure that the Compensator
knows what it is in for and makes preparations
for the upcoming commit—whatever those preparations might be. The nature of
such preparations, is dependent on the details of the CRM implementation.
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 Prepare
phase is about to complete. The
CRM can return TRUE or FALSE indicating whether it has
successfully completed the Prepare
phase. If you return FALSE (or don’t
return anything until the timeout period), COM+ asks all other RMs also
participating in the transaction (if any) to roll back. It also calls the Abort
methods on your CRM (to be discussed soon) and gives the Compensator
the opportunity to undo any changes that the Worker
might have
made.
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 Compensator
repeatedly until it reports a
successful commit.
Phase II: Commit
The Commit
phase is very similar to the Prepare
, except this is
where the CRM is expected to make the Worker’s changes permanent. It is
important to understand that after your Compensator
has completed its Prepare
phase and moves to the Commit
phase, COM+ expects the Compensator
to
be able to commit and might keep rerunning your Compensator
until it
reports to COM+ that the commit ultimately succeeded. You must also realize that COM+ might use a
different instance of your Compensator
in the Prepare
phase than what it uses in the Commit
phase, so don’t keep any
kind of state in member variables, globals, and so on that you need to span the
two phases.
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 Prepare
phase.
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 Worker
has done.
The second argument has a value of TRUE if something
catastrophic happens after the Compensator
completes its Prepare
phase successfully and tries unsuccessfully to commit on
a prior occasion. After the CRM completes its Prepare
phase without indicating
an error, it is on the hook to commit the transaction. Although your Compensator
might take special, additional steps to handle the case of a recovery, you are
still obligated to commit. If your Compensator
crashes again in this or another method call during a recovery, COM+ reruns
your Compensator
yet again and again indicates that it is committing in the context of a
recovery. For an in depth look at recovery, forcing transactions to abort, and
other boundary conditions consult the book’s source code on RM.
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 PrepareRecordVariants()
in the Prepare
phase. COM+ calls this method once for every log record made by
the Worker
.
The Compensator
is to make these changes permanent, and it has the option of removing or
forgetting a record if it is certain that it is complete or deems it no longer
necessary.
Step 3: Commit Changes
EndCommitVariants()
This method notifies the Compensator
that it has received all log
entries. As the Compensator
does not return an error code or
crash, the commitment is complete as far as COM+ is concerned, and the log is
discarded.
Aborting Transactions
Sadly, 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 Compensator
for the CRM can specifically indicate failure in
the Prepare
phase by returning FALSE when EndPrepareVariants()
is called or by crashing. Similarly,
any COM+ object that is manipulating an RM or CRM can call SetAbort()
in
the context of a transaction. In either case, COM+ asks all RMs and CRMs to
roll back their changes.
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:
- ;A COM+ object calls
SetAbort()
. - ;During the
Prepare
phase, the CRM crashes or returns FALSE when COM+ calls itsEndPrepareVariants()
method. - ;Another RM reports failure or fails to check
in during its
Prepare
phase. - ;Your CRM crashes after its
Prepare
phase but before it receives a commit command from the DTC. This is called an in-doubt state, and we talk about it in the section "When In Doubt."
Also note that the CRM abort methods are not called if the CRM
crashes during the Commit
phase. Remember: if the CRM successfully completes
its Prepare phase, aborting the transaction is no longer an option. We talk
about how to handle this case after we take a look at the abortive functions of
ICrmCompensatorVariants.
Step 1: Prepare to Abort
BeginAbortVariants [in] VARIANT_BOOL * recovery)
This method notifies the Compensator
that it should prepare to
abort all changes made by the Worker
.
The recovery argument is TRUE if the Compensator crashes
during or after its Prepare
phase but before it receives a commit command from
the DTC. If it previously aborted gracefully via ForceTransactionToAbort()
,
or the transaction was voted down by a COM+ object, this argument is FALSE.
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 Compensator
can then undo changes made by the Worker, from the latest to the earliest, and can, of
course, remove the record from the log by returning TRUE.
Step 3: Finalizing Abortion
EndAbortVariants()
EndAbortVariants()
notifies the Compensator
that it
has received all log records and the abort is complete.
Handling Recovery
We have discussed that the successful completion of the Prepare
phase is a commitment, and the Compensator
is obligated to commit the
transaction. But what happens if a CRM never can commit its transaction?
Suppose some resource it depends on becomes forever unavailable moments after
the Prepare
phase, forever dooming its attempts to commit. COM+ runs your CRM,
asks it to commit every time a Worker
calls RegisterCompensator()
,
and identifies your particular class of Compensator
.
There is no simple answer. Ideally, you design your Compensator
such that after it has completed the Prepare
phase, it is certain to be able to
complete the commit. However, your Compensator
might rely on some technology
or OS service that proves flaky. To protect against the possibility of never
being able to commit, you can check the recovery argument of BeginCommitVariants()
.
If it is TRUE, you know that the CRM crashed during a previous
attempt to commit. You can keep
track of how many times the commit has failed by adding additional entries to
the log. Remember that the ICrmLogControl interface is given to the Compensator
after it is instantiated (COM+ calls its SetLogControlVariants
method), and so, like the Worker
, it has the capability of adding entries.
These entries can be interpreted by the Compensator on a
subsequent commit attempt; after a number of attempts, your CRM might throw up
its hands and report a completed commit to get COM+ off its back. It might then
send an email to the system administrator or write to some system event log, "Critical Failure: I tried and I tried, but was never able to commit. Data
modifications that certain objects thought happened never did. Human
intervention is required."
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 Compensator
is asked to commit changes more than once, so make sure that entries to the log
are written in such a way as to be idempotent. For example, if the log
indicates that an account should be debited once by $50, there is the
possibility that the Compensator
will end up debiting the account
repeatedly if asked to commit multiple times during the recovery process. It is
better if the log entries indicate the account’s last balance, as well as the
amount it should be debited by. In this way, the Compensator
can check
the account balance, and if it sees the balance is already reduced by $50,
choose not to debit the amount
again. Alternatively, it might be better design for the Worker
to
specify what the new balance of the account should be, including the $50 debit.
Setting the absolute balance is idempotent because no matter how many times you
do it, the effect is same.
When In Doubt
In the process of a two-phase commit, there is a period of time
between the Prepare
phase and the Commit
phase where an RM or CRM can be said
to be in doubt about the ultimate success of the distributed transaction. This
occurs when a transaction is spanning more than one RM on more than one
machine—thus, more than one DTC is involved. If you have a transaction spanning
two machines, A and B, each machine has its own DTC that controls the RM on
that machine. The primary or controlling DTC communicates with the DTCs on other machines,
propagates prepare and
commit notifications to the other DTCs, and coordinates the responses to these
commands. During a distributed transaction, RMs become interdependent as their
DTCs must gather consensus and coordinate with one another to make sure each RM
completes its Prepare
phase.
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 Prepare
phase or crash. If this happens, the prepared RM does not get
a commit command from the DTC; rather, it is asked to roll back. On the other
hand, if all the other RMs do complete their Prepare
phase, the controlling DTC
issues a commit notification, and the prepared RM enters its Commit
phase. The
period of time for an RM, after the preparation is complete but before a commit
command is received from the DTC, is called the in-doubt state because the RM, though ready, is not sure (or is
in doubt) about whether the transaction will ultimately succeed.
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 Prepare
phase, it rolls back all changes made
during the Prepare
phase.
The Complete Compensator
Now that we’ve stepped through
the phases of Compensator
committal and abortion, the complete listing of
our XML Compensator
should make sense. Listing 9.4 details the
implementation of the XML sample Compensator
. The listing is somewhat long, but not
especially complex; the source file comments will provide a running commentary.
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 As IContextState
Dim XMLWorker As XMLWorkerVB
Private Sub Class_Initialize()
'Get an instance of the Object’s context, so we can call
'SetAbort and SetComplete
Set pObjectContext = GetObjectContext()
Set pObjectState = pObjectContext
'Also get an instance of the XMLWorker component we are wrapping:
Set XMLWorker = New XMLWorkerVB
End Sub
'Wrapper functions:
Public Sub AddAccount(AccountNumber As Long, Balance As Double, filename As ÂString)
On Error GoTo addproblem
XMLWorker.AddAccount AccountNumber, Balance, filename
Exit Sub
addproblem:
Call ErrorHandler
End Sub
Public Sub RemoveAccount(AccountNumber As Long, filename As String)
On Error GoTo removeproblem
XMLWorker.RemoveAccount AccountNumber, filename
Exit Sub
removeproblem:
Call ErrorHandler
End Sub
Public Sub CreditAccount(AccountNumber As Long, Amount As Double, filename As ÂString)
On Error GoTo creditproblem
XMLWorker.CreditAccount AccountNumber, Amount, filename
Exit Sub
creditproblem:
Call ErrorHandler
End Sub
Public Sub DebitAccount(AccountNumber As Long, Amount As Double, filename As ÂString)
On Error GoTo debitproblem
XMLWorker.DebitAccount AccountNumber, Amount, filename
Exit Sub
debitproblem:
Call ErrorHandler
End Sub
'The following two methods are exposed so a client application can
'explicitly abort or commit the transaction.
Public Sub Abort()
pObjectContext.SetAbort
End Sub
Public Sub Commit()
pObjectContext.SetComplete
End Sub
'Our errorhandler is executed when a call to the XMLWorker component has failed.
'A common error with wrapper components is to try to execute a method against
'an underlying component has already aborted the transaction:
Private Sub ErrorHandler()
If Err.Number = CONTEXT_E_ABORTING Then
MsgBox "Could not perform the operation. The transaction has been Âaborted."
pObjectContext.SetAbort
Else
MsgBox Err.Number & " : " & Err.Description
End If
End Sub
CRMs and Isolation
One critical property of a transaction, isolation, is not automatically provided by the CRM. Isolation implies that what happens inside a transaction is hidden and protected. Normally, an RM places some form of lock on the resources involved. Most relational databases, for example, provide row level locking so that rows whose data is involved in a transaction are protected from modification by clients outside the transaction. The CRM architecture, however, does not provide any facility to aid the developer in terms of enforcing isolation. This is not to say that synchronization support is not provided—it is. All COM+ transactions are synchronous and have only one logical thread (transactions always run a COM+ Activity, discussed in Appendix B, "COM+ SynchronizationThrough Activities"). But don’t confuse synchronization and concurrency with isolation; they are altogether different.
Although you can be certain that only one method call will execute
on the Worker
of your CRM at any one time, you cannot be certain where that method will come
from. For example, it is possible that two transactions executing concurrently
involve the same CRM. Unlike a true RM whose interfaces have method arguments
to keep track of transaction IDs, the CRM architecture does not include any
direct facility to keep track of what objects are calling into it or what
transactions they are involved in. Thus, Object A from Transaction T1 might ask
an instance of your CRM to make a modification to some region of data in one
method call, and Object B from Transaction T2 might ask for a change to the
same region in another. Although different instances of your CRM might be used,
the same underlying data source is being modified in both cases. The CRM,
therefore, needs to employ some method of protecting specific regions of the
data source involved in a transaction and blocking would-be datamodification
requests from clients outside of that transaction.
You can choose whatever method you want to enforce isolation. File locking, or some other form of OS lock, often works well. Just keep in mind that a CRM is nota singleton, and you cannot count on the same instance of a CRM servicing different clients in different transactions. So, if you are a C++ developer and want to employ mutexes, semaphores, critical sections, and so on, make sure they are named so that they are at system-wide scope and can synchronize access across multiple processes.
The CRMREGFLAG_FAILIFINDOUBTSREMAIN Flag
It is possible that some form of crash, network outage, and so on can result in a CRM that, after recovering, is unsure about the success or failure of a transaction it participated in. If you want to prevent your CRM from being involved in any new transactions while a pre-existing transaction is still in doubt, you need only code the following:
Clerk.RegisterCompensator "MyCompensator.Comp", "[put your description here]",
ÂCRMREGFLAG_ALLPHASES OR CRMREGFLAG_FAILIFINDOUBTSREMAIN
Summary
If a relational database system is capable of handling transactions, and it supports the OLE Transaction two-phase commit protocol, it is considered by COM+ to be an RM. If an RDBMS only supports the XA protocol, but its ODBC driver (or some other form of Resource Dispenser) can convert an XA to an OLE Transaction, this database also qualifies as an RM.
An RM does not necessarily need to be a relational database system, however. The CRM is an example of an RM that can make changes to any structure of data within the context of a COM+ transaction. The CRM is a template and protocol that a developer can use to simplify the complex task of writing an RM.
The author of the CRM must
implement two coclasses: the Worker
and the Compensator
. The Worker
is instantiated and used by any client executable
or object to make changes to some resource kept by a given CRM. When COM+
attempts to commit a transaction involving a CRM, it instantiates an instance
of the Compensator
and interacts with it during the Prepare
and
Commit
phases of a transaction. Typically, the Worker
is responsible for making initial changes of
state to underlying data, and the Compensator
is responsible for bringing this change to its
final state and making such changes permanent. Both the Worker
and Compensator
can
write to a log maintained on their behalf by COM+, but only the Compensator
is
presented with the actual entries of this log. The log, kept by the CRMClerk
object, is the only form of communication between the Worker
and Compensator
,
and COM+ gives an interface (ICrmLogControl) to both objects upon their instantiation.
A Worker
can
explicitly abort the transaction by calling the ForceTransactionToAbort()
method of the CRMClerk
, but the Compensator
may not
call this method. The Compensator
might indicate failure by returning FALSE
during the final steps of the Prepare
phase, or it can simply crash. If,
however, the Compensator
successfully completes its Prepare
phase, it is
obligated to commit its changes if asked.
COM+ does not provide isolation for CRMs. There can be any number
of CRM instances running at a given time, so CRM authors should take care to
protect data relevant in a particular transaction from corruption by another
CRM instance in a different transaction. Additionally, the CRM author must make
sure that actions made by the Compensator
are idempotent—that is, they
can safely be implemented more than once without negative consequence.
Enabling A CRM
For components of a COM+ application to use a CRM, the "Enable Compensating Resource Managers" check box needs to be clicked on the Advanced tab of the application’s (that is, the one hosting the CRM) properties.
For additional configuration settings, including transactional and JIT settings for the worker and compensator, the MSDN offers a concise list so there is no need to duplicate it here. Simply query MSDN for the string, "Installing CRM Components."