Page #95 (COM Support for Transactions)

< BACK  NEXT >
[oR]

The Architecture

Resource Managers

COM+ is an infrastructure. As an infrastructure, it should handle any resource generically; and not know the details of any specific resource.

To access and modify the durable state of a resource in a generic fashion, COM+ relies on a software component called the Resource Manager (RM).

A resource manager is a software component that has an intimate knowledge of a specific type of resource, such as a relational database. Under the influence of a transaction, the RM keeps track of the changes made to the resource. If the transaction aborts, the RM can roll back these changes on the resource and bring it back to the original state. A simple RM, for example, may buffer the changes made to the resource and persist the changes only if the transaction commits.

There are many commercially available RMs, including the ones for Microsoft SQL Server, Oracle, IBM DB2, Informix, and Sybase. The database server used in the simulation program, MSDE, also provides its own RM.

When a client instantiates an RM, the client gets a proxy to the RM. OLE DB drivers and ODBC drivers are examples of RM proxies. The RM proxy provides APIs to access the RM. Typically, the RM proxy provides COM interfaces, although it is not a requirement. ODBC drivers, for example, do not provide COM interfaces.

graphics/01icon01.gif

An RM proxy is typically implemented as part of another software component called the Resource Dispenser (RD). Unlike a resource manager that manages the durable state of a resource, a resource dispenser manages the non-durable state of the resource, such as the number of connections to a resource. We will cover resource dispensers in Chapter 11 when we discuss scalability issues.


A transaction can involve many resource managers who may span multiple machines across the network. If some operation in a transaction fails, all the participating resource managers need to be informed so that the changes to the resources can be rolled back. This implies that some service should exist that can coordinate all the resource managers involved in the distributed transaction. This service does exist and is called the Microsoft Distributed Transaction Coordinator (MS-DTC).

The Distributed Transaction Coordinator

As the name implies, the Distributed Transaction Coordinator (DTC) coordinates a transaction that could potentially be distributed across the network. More precisely, the DTC manages the resource managers. Based on the outcome of a transaction, it informs each of the participating resource managers to either abort or commit the changes to their respective resources.

Each system that needs to use transactions must have the DTC installed. If you install MS SQL Server or MSDE, the MS-DTC automatically gets installed.

graphics/01icon01.gif

The MS-DTC is a Windows NT ser vice that can be started and stopped from the service control panel or from the MS-DTC property page in the Component Services snap-in.


A non-transactional client (transactional clients let COM+ manage their transactions) can obtain the DTC using the SDK API DtcGetTransactionManager and explicitly request to begin a new transaction. The following code snippet illustrates the process of using the DTC:

 CComPtr<ITransactionDispenser> spTxDisp;  HRESULT hr = DtcGetTransactionManager(   NULL,                                   // host name    NULL,                                   // TM name    __uuidof(ITransactionDispenser),        // interface    0,                                      // reserved    0,                                      // reserved    0,                                      // reserved    (void**) &spTxDisp);                    // [out] pointer  CComPtr<ITransaction> spTx;  hr = spTxDisp->BeginTransaction(   NULL,                                   // outer component    ISOLATIONLEVEL_ISOLATED,                // Isolation level    ISOFLAG_RETAIN_DONTCARE,                // Isolation flag    NULL,                                   // Options    &spTx);                                 // [out] pointer  ... // Enlist RMs and perform resource updates  if (bSuccess) {   spTx->Commit(0, XACTTC_SYNC_PHASEONE, 0);  }else {   spTx->Abort(NULL, 0, FALSE);  } 

When the non-transactional client requests a new transaction, the DTC (more precisely, a part of the DTC called the transaction manager) dispenses the transaction as a pointer to interface ITransaction. The client can then enlist other appropriate RMs to participate in the transaction.

This COM+ mechanism of letting a non-transactional component handle a transaction manually is referred to as Bring Your Own Transaction (BYOT). An interesting use of BYOT is to manually create a transaction with an arbitrary, long timeout [Mar-00].

The DTC identifies each transaction uniquely as a C structure of type XACTUOW. A client can obtain this identification by calling ITransaction::GetTransactionInfo. As we will see later, COM+ reinterprets this structure as a GUID.

To commit a transaction, the client calls ITransaction::Commit. At this point, the DTC requests each of the enlisted RMs to commit its changes.

What if one of the RMs run into some internal problem and fails to commit?

Two-Phase Commit

To ensure that the all-or-nothing proposition is maintained for a transaction, the DTC mandates that each RM attempt the commitment in two phases, prepare and commit.

In the prepare phase, the RM should do everything it takes to ensure that the commit phase does not fail. It is up to the developer of the RM to define what everything means and how to make it happen. All possible internal problems that an RM can run into should be returned as an appropriate error in this phase.

If no problems are encountered during the prepare phase, the RM saves all of its state information in such a manner that failure to commit can no longer occur. It then returns from the prepare phase with a successful status indicating that it is ready to make the changes permanent.

In the commit phase, the RM applies the just-saved state information and makes the changes permanent. This phase should not fail, unless it runs into some catastrophe such as a power shutdown.

With the breakup of a transaction commitment into two phases, the interaction between the DTC and the RMs gets simplified. The following is the algorithm:

  • The client requests the DTC to commit the transaction.

  • The DTC sends a prepare request to each RM that has been enlisted in the transaction. Upon receiving this request, an RM prepares its internal state.

  • If any RM returns a failure status during the prepare phase, the DTC informs the rest of the RMs to abort their changes.

  • If all the RMs respond positively to the prepare request, the DTC requests each RM to commit its changes.

What if the RM runs into a catastrophic failure, such as a power shutdown, in the middle of the commitment phase?

It is the responsibility of the RM to persist its internal state after the prepare phase so that it will survive a system failure. Recall that this requirement comes from the durability property of the ACID test.

This brief introduction to the DTC and RM is enough for our current discussion. For more information, check out Richard Grimes book, Professional Visual C++ 6 MTS Programming [Gri-99], that has one whole chapter dedicated to the DTC. Also, see Jonathan Pinnock s book, Professional DCOM Application Development [Pin-98], for an example of developing your own resource manager and resource dispenser.

Let s see how COM+ supports transactions automatically.

Automatic Transactions through COM+

A configured component indicates its interest in participating in a transaction by means of the transaction attribute.

When an object from such a component is activated, COM+ sets up the object s context to handle transactions.

COM+ automatically begins a transaction when it encounters either of the following conditions:

  1. When a non-transactional client activates an object whose component has its transaction attribute set to either the TRANSACTION_REQUIRED or TRANSACTION_REQUIRES_NEW values.

  2. When a transactional client calls an object whose component has its transaction attribute set to the TRANSACTION_REQUIRES_NEW value.

The object responsible for beginning a new transaction is referred to as the root object of that transaction. As we will see shortly, this root object has a special role in completing the transaction.

As a corollary, an object whose transaction attribute is set to TRANSACTION_REQUIRES_NEW will always be a root object.

When the root object is activated, COM+ transparently asks the DTC for a new transaction. The DTC returns an ITransaction pointer that COM+ stores in the object s context.

An object that subsequently gets activated within the boundary of this transaction, and is marked as either TRANSACTION_REQUIRED or TRANSACTION_SUPPORTED, will share the transaction.

A collection of one or more contexts that share a transaction is referred to as a transaction stream.

To ensure that all the contexts within a transaction stream share only one transaction at a time, COM+ mandates a component that requires a transaction should also require synchronization. Recall from Chapter 5 that COM+ sets up such a component to run under an activity.

More precisely, a transaction stream is completely contained inside an activity, but an activity can contain more than one transaction stream.

If an object is participating in a transaction, it can obtain its transaction ID from its context, as shown in the following code fragment:

 CComPtr<IObjectContextInfo> spInfo;  HRESULT hr = CoGetObjectContext(__uuidof(IObjectContextInfo),    (void**) &spInfo);  _ASSERT (SUCCEEDED(hr));  GUID tid;  hr = spInfo->GetTransactionId(&tid);  _ASSERT (SUCCEEDED(hr)); 

Note that COM+ returns the transaction ID as a GUID, and not as a XACTUOW structure.

When a transactional object accesses a transactional resource for the first time, the data access layer (such as ODBC and OLE DB) accesses the context s transaction automatically and enlists the corresponding RM with the DTC. In our simulation program, for example, when the account manager object opens the AccountsDB database using ADO, the underlying OLE DB driver (SQLOLEDB) enlists the MSDE resource manager with the DTC in the context of the current transaction. This auto-enlistment feature provided by the data access layer simplifies code development and is fundamental to the declarative programming model of COM+.

Each component participating in the transaction casts its vote by calling IContextState::SetMyTransactionVote, a method that we have already seen in action.

A transaction completes when the root object of the transaction is deactivated. At this point, COM+ checks to see if all the objects have individually given their consent to commit the transaction. Depending on the consensus, it either calls ITransaction::Commit or ITransaction::Abort on the current transaction.

graphics/01icon01.gif

The transactional objects themselves do not participate in the two-phase commit process; only the enlisted RMs do. In fact, the transactional objects do not even know about the commitment process, nor do they care.


A transaction also completes when it exceeds its timeout threshold. Transactions are generally designed to be short-lived, as locking a resource for an extended period of time can cause bottlenecks in the system. To ensure efficient performance, COM+ defines a global timeout period for transactions. The default is 60 seconds, but an administrator can change it to any suitable value. COM+ also provides a configuration setting to override the global timeout value for individual components.

If a transaction exceeds its timeout threshold, COM+ will deactivate all the participating objects and abort the transaction.

graphics/01icon01.gif

COM+ 1.0 (the current release) uses a serializable level of isolation for transactions. This level of isolation enforces highest level of locking on the underlying resource, thereby providing the highest degree of data integrity. In general, the higher the level of resource locking, the lower the scalability of the application. Under COM+ 1.x (the future release) you will be able to configure the isolation level on a per-component basis. The root object gets to dictate the isolation level for the transaction. Be aware though, that a lower level of isolation increases the chances of incorrect data.


Earlier, I said that, in order to commit a transaction, all the objects participating in the transaction need to cast a positive vote. An obvious improve-ment that can be made is that, instead of requiring all the participating objects to cast a positive vote, it is sufficient that any one participating object casts a negative vote. This in fact is the default behavior under COM+. The implication of this is that the developers of a transactional component need not call SetMyTransactionVote(TxCommit) on successful operations. They just need to indicate only the failure status (via TxAbort).

Lifetime of a Transaction

Consider the following base client VBScript code:

 set TradeMgr = CreateObject("TradeMgmt.TradeMgr")  TradeMgr.BuyStocks "Don", "INTC", 100  TradeMgr.BuyStocks "Chris", "MSFT", 1000  TradeMgr = NULL 

Recall that Chris does not have enough funds to buy 1000 shares of MSFT; Don, however, does have enough funds to cover 100 shares of INTC. However, if you execute the above code and check the values stored in the database, you will find that even Don was unable to buy the shares he wanted. What went wrong?

Recall that a transaction is considered complete only after the root object of the transaction gets deactivated. In the above lines of code, the root object gets deactivated after executing the two BuyStocks statements. As a result, both BuyStocks statements are considered to be part of the same transaction. When the second BuyStocks statement failed, all the changes, including the one from the first BuyStocks statement, were rolled back.

An obvious solution is to release the root object after the first call to BuyStocks and immediately recreate it before making the second call.

Though the proposed technique will work, releasing an object and recreating it each time is very inefficient.

Fortunately, COM+ offers a better solution.

COM+ provides a way to deactivate an object even if the base client has not released it. To make this possible, COM+ always returns a proxy pointer to the base client, instead of returning the actual reference to the object. This provides COM+ the flexibility to deactivate the actual object while keeping the proxy alive. When the base client makes a method call on the proxy, COM+ can transparently reactivate the object. This is referred to as just-in-time (JIT) activation.

JIT is covered in detail in Chapter 11 when we discuss scalability. The important point to note here is that COM+ enforces a component that requires a transaction to have JIT enabled.

graphics/01icon01.gif

COM+ will automatically enforce JIT Activation to TRUE and Synchronization as REQUIRED for any component marked as TRANSACTION_REQUIRED or TRANSACTION_REQUIRES_NEW.


An object that is JIT-enabled contains a bit in its context called the done bit or, more precisely, the deactivate-on-return bit. COM+ checks this bit after its return from each method call. If the bit is turned on, COM+ will deactivate the object. By default, COM+ turns this bit off before entering a method. However, one can change this behavior at the interface method level from the Component Services snap-in.

The deactivate-on-return bit can also be set programmatically by using the method SetDeactivateOnReturn available on the interface IContextState. The following is its prototype:

 Interface IContextState : IUnknown  {   ...    HRESULT SetDeactivateOnReturn(VARIANT_BOOL bVal);  } 

Using this method, method CTradeMgr::BuyStocks can be revised to deactivate the object on return, as shown in the following code fragment:

 STDMETHODIMP CTradeMgr::BuyStocks(BSTR bsClient, BSTR bsSymbol,    long lShares)  {   CComPtr<IContextState> spState;    HRESULT hr = ::CoGetObjectContext(__uuidof(IContextState),      (void**) &spState);    if (FAILED(hr)) {     return hr;    }    hr = spState->SetDeactivateOnReturn(VARIANT_TRUE);    _ASSERT (SUCCEEDED(hr));    try {     //      // First operation - Obtain the stocks.      //      IStockMgrPtr spStockMgr(__uuidof(StockMgr));      long lAmount = spStockMgr->BuyStock(bsSymbol, lShares);      //      // Second operation - Debit the clien't account balance      //      IAccountMgrPtr spAccountMgr(__uuidof(AccountMgr));      spAccountMgr->Debit(bsClient, lAmount);    }catch(_com_error& e) {     spState->SetMyTransactionVote(TxAbort);      return Error(static_cast<LPCTSTR>(e.Description()),        GUID_NULL, e.Error());    }    spState->SetMyTransactionVote(TxCommit);    return S_OK;  } 

With this change in place, if you execute the base client VBScript code once again, you will see that this time Don s trade would go through and Chris trade would fail, just as expected.

Manual Transactions

Allowing COM+ to automatically manage a transaction simplifies component development. However, there are times when the base client would like to control the outcome of a transaction.

To handle this, COM+ provides a component called the TransactionContext class, represented by the PROGID TxCtx.TransactionObject.

The TransactionContext object supports interface ITransactionContext. Following is its definition, along with a short explanation for each interface method:

 ITransactionContext : IDispatch  {   HRESULT CreateInstance([in] BSTR pszProgId,      [retval][out] VARIANT *pObject);      // instantiate an object    HRESULT Commit();                       // commit a transaction    HRESULT Abort();                        // abort a transaction  }; 

By calling the methods on the ITransactionContext interface, the base client can begin a transaction, compose the work of one or more COM+ components in the transaction, and explicitly commit or abort the transaction. This is illustrated in the following VBScript code snippet:

 Dim txCtx  Set txCtx = CreateObject("TxCtx.TransactionContext")  Dim Accounts  set Accounts = txCtx.CreateInstance("AccountMgmt.AccountMgr")  Accounts.Debit "Don", 10  txCtx.Commit  msgbox "Done" 

Note that an object that is activated by calling ITransactionContext::CreateInstance should belong to a COM+ configured component. Each activated object should cast its transaction vote using the context object. However, using the transaction context, the base client also can participate in the voting process.

Also notice the distinction between an object context and a transaction context. An object context relates to an individual object whereas a transaction context is related to the overall transaction.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net