Designing Transactional Components

Incorporating correct transaction support in your component is an integral part of your design and cannot be done as an afterthought. Supporting transactions is far from simply selecting the correct radio button in the Component Services Explorer. In particular, your object has to be state-aware, actively manage its state, and control its own activation and deactivation, as described in previous sections. You should also design your interfaces to support transactions and to acquire resources in a particular order.

Designing Transactional Interfaces

Interface design is an important factor in designing transactional components. From the object’s perspective, method calls demarcate transactions, so you should avoid coupling interface methods to each other. Each method should contain enough parameters for the object to perform its work and decide whether the transaction should commit or abort. In theory, you could build a transactional component that votes on the transaction outcome only after receiving a few method calls. However, in practice, a transaction should not span multiple method calls. You already saw that a transactional object uses JITA and should deactivate itself at method boundaries. COM+ checks the object’s vote once it is deactivated. If the interface the object implements requires more than one method call for the object to decide on its vote, then the object could not deactivate itself; it must wait for another call from the client. What should the object do if the transaction suddenly ends (because the root was deactivated or the transaction timed out) and the anticipated call from the client never comes?

Waiting for additional information from the client has a serious effect on overall application throughput. While your transaction is in progress, the resource managers involved lock out all other transactions from the data being modified by your transaction. The other transactions are blocked until your transaction commits or aborts. The longer you wait for client calls that may never come, the more your application’s throughput will suffer.

Consider, for example, a poorly designed interface used to handle customer orders:

[
 helpstring("Bad design of IOrder interface"),
]
interface IOrder : IUnknown
{
  HRESULT SetOrder([in]DWORD dwOrderNumber);
  HRESULT SetItem([in]DWORD dwItemNumber);
  HRESULT SetCustomerAccount([in]DWORD dwCustomerAccount);
  HRESULT ProcessOrder(  );
};

The interface designer intends for the client to call the Set( ) methods, supplying the object with the order parameters, and then call ProcessOrder( ). The problem with this design is that the transactional object cannot vote on the transaction outcome unless the client calls all the Set( ) methods and then the ProcessOrder( ) method, in that sequence. There is no clear delineation of transaction boundaries in this interface design.

The correct way to design the interface while maintaining transaction semantics is:

[
 helpstring("Correct design of IOrder interface"),
]
interface IOrder : IUnknown
{
  HRESULT ProcessOrder([in]DWORD dwOrderNumber,[in]DWORD dwItemNumber,
                       [in]DWORD dwCustomerAccount);
};

This interface is also a lot easier to implement. The order number is used to identify the object and allow it to retrieve its corresponding state from the resource manager—in this case, the orders database:

STDMETHODIMP COrder::ProcessOrder(DWORD dwOrderNumber,DWORD wItemNumber,
                                   DWORD dwCustomerAccount)
{
   HRESULT hres = S_OK;
   GetState(dwOrderNumber);//retrieve the state of the corresponding 
                           //order object 
   hres = DoProcessOrder(wItemNumber,dwCustomerAccount);
   SaveState(dwOrderNumber);
   
   
   // Using auto-deactivation. No need to vote explicitly.  
   return hres;
}

The second interface design yields better performance as well, because there are fewer calls to the object from the client machine, which may be across the network.

Acquiring Resources

Suppose you have two transactions, T1 and T2, that execute in parallel, and both require access to two resource managers, RM1 and RM2. Suppose T1 has acquired RM1, and T2 has acquired RM2. What would happen if T1 tries to access RM2, and T2 tries to access RM1? You would have a deadlock. Neither transaction is able to proceed. Each needs a resource the other holds to complete its work. Each is blocked and never frees the resource manager it holds.

The solution to this deadly embrace is to be mindful about the order in which objects in your transaction try to acquire resources. You can avoid the deadlock by always trying to acquire the resources in the same order. In the previous example, if both transactions try to acquire RM1 and then RM2, then the first one to actually acquire RM1 will continue on to acquire RM2; the second transaction will be blocked, as it waits for RM1 to be released.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset