Click here to Skip to main content
Click here to Skip to main content

Applying COM+ - Chapter 9: Compensating Resource Managers

, 19 Mar 2001
Rate this:
Please Sign up or sign in to vote.
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.
<!-- Article image -->
Sample Image
Title Applying COM+
Authors Gregory Brill
PublisherNew Riders
PublishedOctober 2000
ISBN 0735709785
Price US 49.99
Pages 450

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 its EndPrepareVariants() 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."

Copyright © 2000 New Riders Publishing

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

New Riders

United States United States
No Biography provided

Comments and Discussions

 
GeneralActive Directory PinsussAnonymous26-Nov-04 5:13 
GeneralRe: Active Directory PinsussAnonymous13-Dec-04 3:26 
GeneralWow! PinmemberDigitalBay20-Nov-04 12:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Info
| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 20 Mar 2001
Article Copyright 2001 by New Riders
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid