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.
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.
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.
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 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.
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.
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.
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.
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.