Transactional Object Life Cycle

If a transaction aborts, the intermediate and potentially inconsistent state of the system should be rolled back to ensure consistency. The system state is the data in the resource managers; it also consists of the state of all the objects that took part in the transaction. An object’s state is the data members it holds. If the object participated in an aborted transaction, then that object’s state should be purged too. The object is not allowed to maintain that state, since it is the product of activities that were rolled back.

The problem is that once a transaction ends, even if the object votes to commit, it does not know whether that transaction will actually commit. The DTC still has to collect all the resource managers’ votes, conduct the first phase of the two-phase commit protocol, and verify that all of the resource managers vote to commit the transaction. While this process takes place, the object must not accept any new client calls (as part of a new transaction) because the object would act on a system state that may roll back, which would jeopardize consistency and isolation.

To enforce consistency and isolation, once a transaction ends, regardless of its outcome, COM+ releases all the objects that took part in it. COM+ does not count on objects’ having the discipline or knowledge to do the right thing. Besides, even with good intentions, how would the objects know exactly what part of their state to purge?

However, even though the objects are deactivated and released, COM+ remembers their position in the general layout of the transaction: who the root was, who created whom, pointers between objects, and the context, apartment, and process each object belongs to.

When a new method call from the client comes into an object (usually to the root object) that was deactivated at the end of a transaction, COM+ creates a new transaction for that method call and a new instance of the object. COM+ then forwards the call to the new instance. If the object tries to access other objects in the transaction, COM+ re-creates them as well.

In short, COM+ starts a new transaction with new objects in the same transaction layout , also called a transaction stream . The transaction itself is a transient, short-lived event; the layout can persist for long periods of time. Only when the client explicitly releases the root will the objects really be gone and the transaction layout destroyed.

State-Aware Objects

Because COM+ destroys any object that took part in a transaction at the end of the transaction, transactional objects have to be state-aware, meaning they manage their state actively. A state-aware object is not the same as a stateless object. First, as long as a transaction is in progress, the object is allowed to maintain state in memory. Second, the object is allowed to maintain state between transactions, but the state cannot be stored in memory or in the filesystem. Between transactions, a transactional object should store its state in a resource manager. When a new transaction starts, the newly created object should retrieve its state from the resource manager. Accessing the resource manager causes it to auto-enlist with that transaction. When the transaction ends, the object should store its modified state back in the resource manager.

Now here is why you should go though all this hassle: if the transaction aborts, the resource manager will roll back all the changes made during the transaction—in this case, the changes made to the object state. When a new transaction starts, the object again retrieves its state from the resource manager and has a consistent state. If the transaction commits, then the object has a newly updated consistent state. So the object does have state, as long as the object actively manages it.

The only problem now is determining when the object should store its state in the resource manager. When the object is created and placed in a transaction, it is because some other object (its client) tries to invoke a method call on the object. When the call returns, it can be some time until the next method call. Between the two method invocations, the root object can be released or deactivated, ending the transaction. COM+ releases the object, and the object would be gone without ever storing its state back to the resource manager.

The only solution for the object is to retrieve its state at the beginning of every method call and save it back to the resource manager at the end of the method call. From the object’s perspective, it must assume that the scope of every transaction is the scope of one method call on it and that the transaction would end when the method returns. The object must therefore also vote on the transaction’s outcome at the end of every method.

Because from the object’s perspective every method call represents a new transaction, and because the object must retrieve its state from the resource manager, every method definition must contain some parameters that allow the object to find its state in the resource manager. Because many objects could be of the same type accessing the same resource manager, the object must have some key that identifies its state. That key must be provided by the object’s client. Typical object identifiers are account numbers and order numbers. For example, the client creates a new transactional order-processing object, and on every method call the client must provide the order number as a parameter, in addition to other parameters. Between method calls, COM+ destroys and re-creates a new instance to serve the client. The client does not know the difference because the two instances have the same consistent state.

Example 4-2 shows a generic implementation of a method on a transactional object. A transactional object must retrieve its state at the beginning of every method and save its state at the end. The object uses an object identifier provided by the client to get and save its state.

The method signature contains an object identifier parameter used to get the state from a resource manager with the GetState( ) helper method. The object then performs its work using the DoWork( ) helper method. Then the object saves its state back to the resource manager using the SaveState( ) method, specifying its identifier. Finally, the object votes on the transaction outcome based of the success of the DoWork( ) method.

Example 4-2. Implementing a method on a transactional object

STDMETHODIMP CMyComponent::MyMethod(PARAM objectIdentifier)
{
   HRESULT hres = S_OK;
   GetState(objectIdentifier);
   hres = DoWork(  );
   SaveState(objectIdentifier);
//Vote on the transaction outcome
   IContextState* pContextState = NULL;
   ::CoGetObjectContext(IID_IContextState,(void**)&pContextState);
   ASSERT(pContextState!= NULL);//Not a configured component
   
   if(FAILED(hres))
   { 
      hres = pContextState->SetMyTransactionVote(TxAbort);
      ASSERT(hres != CONTEXT_E_NOTRANSACTION);//No transaction support 
      hres = CONTEXT_E_ABORTING;
   }
   else
   {
      hres = pContextState->SetMyTransactionVote(TxCommit);
      ASSERT(hres != CONTEXT_E_NOTRANSACTION);//No transaction support
   
   }   
   pContextState->Release(  );
   return hres;
}

Note that not all of the object’s state can be saved by value to the resource manager. If the state contains pointers to other COM+ objects, GetState( ) should create those objects and SaveState( ) should release them. Similarly, if the state contains such resources as database connection, GetState( ) should acquire a new connection and SaveState( ) should release the connection.

Transactions and JITA

If the object goes through the trouble of retrieving its state and saving it on every method call, why wait until the end of the transaction to destroy the object? The transactional object should be able to signal to COM+ that it can be deactivated at the end of the method call, even though the transaction may not be over yet. If the object is deactivated between method calls, COM+ should re-create the object when a new method call from the client comes in.

The behavioral requirements for a state-aware transactional object and the requirements of a well-behaved JITA object are the same. As discussed in Chapter 3, a well-behaved JITA object should deactivate itself at method boundaries, as well as retrieve and store its state on every method call. Since COM+ already has an efficient mechanism for controlling object activation and deactivation (JITA), it makes perfect sense to use JITA to manage destroying the transactional object and reconnecting it to the client, as explained in Chapter 3.

Every COM+ transactional component is also a JITA component. When you configure your component to require a transaction (including Supported), COM+ configures the component to require JITA as well. You cannot configure your component to not require JITA because COM+ disables the JITA checkbox.

At the end of a method call, like any other JITA object, your transactional object can call IContextState::SetDeactivateOnReturn( ) to set the value of the done bit in the context object to TRUE, signaling to COM+ to deactivate it, as shown in Example 4-3.

Example 4-3. A transactional object deactivating itself at the end of the method

STDMETHODIMP CMyComponent::MyMethod(PARAM objectIdentifier)
{
   HRESULT hres = S_OK;
   GetState(objectIdentifier);
   hres = DoWork(  );
   SaveState(objectIdentifier);

   IContextState* pContextState = NULL;
   ::CoGetObjectContext(IID_IContextState,(void**)&pContextState);
   ASSERT(pContextState!= NULL);//Not a configured component
   
   if(FAILED(hres))
   { 
      hres = pContextState->SetMyTransactionVote(TxAbort);
      ASSERT(hres != CONTEXT_E_NOTRANSACTION);//No transaction support 
      hres = CONTEXT_E_ABORTING;
   }
   else
   {
      hres = pContextState->SetMyTransactionVote(TxCommit);
      ASSERT(hres != CONTEXT_E_NOTRANSACTION);//No transaction support
   
   }
   hres = pContextState->SetDeactivateOnReturn(TRUE);
   pContextState->Release(  );
   return hres;
}

The done bit is set to FALSE by default. If you never set it to TRUE, your object is destroyed only at the end of the transaction or when its client releases it. If the object is the root of a transaction, self-deactivation signals to COM+ the end of the transaction, just as if the client released the root object. Of course, by combining transactions with JITA you gain all the benefits of JITA: improved application scalability, throughput, and reliability.

Collecting Objects’ Votes

Using JITA has a side effect on your object’s transaction vote. When the object is deactivated, the transaction could end while the object is not around to vote. Thus, the object must vote before deactivating itself. When a method call returns, COM+ checks the value of the done bit. If it is TRUE, COM+ checks the value of the consistency bit, the object’s vote.

COM+ collects the objects’ votes during the transaction. Each transaction has a doomed flag, which if set to TRUE dooms a transaction to abort. COM+ sets the value of a new transaction’s doomed flag to FALSE.

When an object is deactivated and its vote was to commit, COM+ does not change the current value of the doomed flag. Only if the vote was to abort will COM+ change the doomed flag to TRUE. As a result, once set to TRUE, the doomed flag value will never be FALSE again, and the transaction is truly doomed.

When the root object is deactivated/released, COM+ starts the two-phase commit protocol only if the doomed flag is set to FALSE. Note that COM+ does not waste time at the end of a transaction polling objects for their vote. COM+ already knows their vote via the doomed flag.

The IObjectContext Interface

The context object supports a legacy MTS interface, called IObjectContext, defined as:

interface IObjectContext : IUnknown 
{
   HRESULT CreateInstance([in]GUID* rclsid,[in] GUID* riid,[out,retval]void** ppv);
   HRESULT SetComplete(  );
               HRESULT SetAbort(  );
   HRESULT EnableCommit(  );
   HRESULT DisableCommit(  );
   BOOL IsInTransaction(  );
   BOOL IsSecurityEnabled(  );
   HRESULT IsCallerInRole([in]BSTR bstrRole,[out,retval]BOOL* pfIsInRole);
};

IObjectContext is worth mentioning only because most of the COM+ documentation and examples still use it instead of the new COM+ interface, IContextState .

IObjectContext has two methods used to vote on a transaction outcome and to control object deactivation. Calling SetComplete( ) sets the consistency and done bits to TRUE. SetComplete( ) sets the vote to commit and gets the object deactivated once the method returns. SetAbort( ) sets the vote to abort the transaction and sets the done bit to TRUE, causing the object to deactivate when the method returns. COM+ objects should avoid using IObjectContext and should use IContextState instead. IContextState is fine-tuned for COM+ because it sets one bit at a time. It also verifies the presence of a transaction—it returns an error if the object is not part of a transaction.

COM+ objects written in VB 6.0 have no way of accessing IContextState directly. They have to go through IObjectContext first and query it for IContextState, as shown in Example 4-4. Objects written in Visual Basic.NET can access IContextState directly.

Example 4-4. Querying IObjectContext for IContextState

Dim objectContext As ObjectContext
Dim contextState As IContextState

Set objectContext = GetObjectContext

'QueryInterface for IContextState: 
Set contextState = objectContext
contextState.SetMyTransactionVote TxCommit

Method Auto-Deactivation

As shown in Chapter 3, you can configure any method on a JITA object to automatically deactivate the object when it returns.

Configuring the method to use auto-deactivation changes the done bit from its default value of FALSE to TRUE. Because the default value for the consistency bit is TRUE, unless you change the context object bits programmatically, auto-deactivation automatically results in a vote to commit the transaction.

However, COM+ examines the HRESULT that the method returns. If the HRESULT indicates failure, then the interceptor sets the consistency bit to FALSE, as if you voted to abort. This behavior gives you a new programming model for voting and deactivating your object: if you select auto-deactivation for a method, don’t take any effort to set any context object bits. Instead, use the method’s returned HRESULT:

  • If it is S_OK, it is as though you voted to commit. (S_FALSE would also vote to commit.)

  • If it indicates failure, it is as though you voted to abort.

When you use auto-deactivation, the programming model becomes much more elegant and concise, and shown in Example 4-5. With auto-deactivation, the object does not have to explicitly vote on the transaction’s outcome or deactivate itself. Compare this with Example 4-3. Both have the same effect, but note how elegant, readable, and concise Example 4-5 is.

Example 4-5. Using method auto-deactivation

STDMETHODIMP CMyComponent::MyMethod(PARAM objectIdentifier)
{
   HRESULT hres = S_OK;
   GetState(objectIdentifier);
   hres = DoWork(  );
   SaveState(objectIdentifier);
   return hres;
}

Additionally, the object’s client should examine the returned HRESULT. If it indicates failure, then it also indicates that the object voted to abort the transaction; the client should not waste any more time on the transaction because it is doomed.

Object Life Cycle Example

The following simple example demonstrates the important concepts discussed in this section. Suppose a nontransactional client creates Object A, configured with transaction support set to Required. Object A creates Object B, which also requires a transaction. The developers of Object A and Object B wrote the code so that the objects vote and get themselves deactivated on method boundaries. The client calls two methods on Object A and releases it. Object A then releases Object B.

When the client creates Object A, COM+ notes that the client does not have a transaction and that Object A needs transaction support, so COM+ creates a new transaction for it, making Object A the root of that transaction. Object A then goes on to create Object B, and Object B shares Object A’s transaction. Note that Object B is in a separate context because transactional objects cannot share a context. Now the transaction layout is established. The transaction layout persists until the client releases Object A, the root of this transaction. Note that both the client and the objects have references to cross-context interceptors, not to actual objects. While a call from the client is in progress, both objects exist (see Figure 4-9) and the transaction layout hosts an actual transaction.

Transaction layout while a transaction is in progress

Figure 4-9. Transaction layout while a transaction is in progress

However, between the two method calls from the client, only the transaction layout is maintained; no objects or a transaction are in progress, only interceptors and contexts (see Figure 4-10). When the second call comes in, COM+ creates Object A, and Object A retrieves its state from the resource manager. When Object A accesses Object B to help it process the client request, COM+ creates Object B and hooks it up with the interceptor Object A is using (see Figure 4-9). When the call comes to Object B, it too retrieves its state from the resource manager. When the method returns from Object B, Object B deactivates itself; when the method returns to the client, Object A deactivates itself. When the client releases its reference to Object A, the transaction layout is destroyed, along with the contexts and the interceptors.

Transaction layout between method calls

Figure 4-10. Transaction layout between method calls

Get COM & .NET Component Services now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.