COM and Declarative Transactions

[Previous] [Next]

COM+ makes it much easier to run distributed transactions using the DTC by providing a programming model based on declarative transactions. You are responsible for creating objects in the scope of a COM+ transaction. When you do this, the COM+ runtime interacts with the DTC to create a distributed transaction. COM+ also enlists your RM connections behind the scenes.

After you create your objects in a transaction, you can use a handful of methods supplied by the ObjectContext interface or the IContextState interface to control the outcome of the transaction. As you'll see, you're responsible for releasing each transaction as soon as possible.

Let's start by examining how to create one or more objects in a COM+ transaction. Every configured component has a Transaction attribute. Figure 8-5 shows the Transactions tab of a component's Properties dialog box, which is available through the Component Services administrative tool. You can use this tab to view and modify the Transaction attribute for each component in a COM+ application. If you're using Visual Basic 5, you must first install your components and then adjust the Transaction attribute by hand.

Figure 8-5 When the COM+ runtime creates an object from a component, it looks at the component's Transaction attribute to determine whether the object should be created in a new transaction or an existing transaction.

Visual Basic 6 added MTSTransactionMode to the property sheet of each public class module in an ActiveX DLL project. When you set this property, as shown in Figure 8-6, Visual Basic publishes the corresponding COM+ Transaction attribute in your server's type library. When you install your DLL in a COM+ application, the catalog manager automatically configures your components with the appropriate Transaction attribute.

Figure 8-6 When you install a DLL in a COM+ application, each component is configured with the Transaction attribute that corresponds to its MTSTransactionMode property.

Notice the difference between the MTSTransactionMode property values for a Visual Basic class module and the Transaction attribute values in COM+. Don't let this variation bother you; it's just a slight difference in the wording between Visual Basic and COM+. Each of the four MTSTransactionMode property settings has a corresponding Transaction attribute setting, as shown in Table 8-2.

Table 8-2 MTSTransactionMode Property Settings and Equivalent COM+ Transaction Attribute Settings

MTSTransactionMode Property Setting COM+ Transaction Attribute Setting
RequiresTransaction Required
RequiresNewTransaction Requires New
UsesTransaction Supported
NoTransactions Not Supported
NotAnMTSObject (no debugging) N/A
N/A Disabled

Creating Objects in a COM+ Transaction

When the COM+ runtime creates a new object, it examines the component's Transaction attribute to see whether the new object should be created in a transaction. When the COM+ runtime creates an object that must run in a transaction, it also determines whether it should activate the object in a new transaction or in the transaction of its creator. In Chapter 7, I explained how COM+ matches objects to activities based on the Synchronization attribute. COM+ matches objects to transactions by using the Transaction attribute in the same manner.

Once an object is created in a transaction, it spends its entire lifetime there. When the transaction is committed or aborted, the COM+ runtime deactivates all the objects inside it by using the just-in-time activation scheme. This means that a Visual Basic object in a transaction must be destroyed when the transaction is released. As you'll see, this cleanup activity is essential to achieving the proper isolation semantics of a transaction. The objects and the state they've acquired must be destroyed at transaction boundaries to enforce the isolation requirement of the ACID rules.

A transactional object can be created by another object in the same COM+ application or by an external client. When the COM+ runtime receives an activation request to create a new object, it determines whether the object's creator is running in an existing COM+ transaction. It also inspects the Transaction attribute of the component. From these two pieces of information, it can determine how to proceed. A component can have one of the following Transaction attribute settings:

  • Required The object is always created in a transaction. The object is placed in the transaction of its creator if one exists. If the creator isn't running in a transaction, the COM+ runtime creates a new transaction for the object.
  • Requires New The COM+ runtime creates a new transaction for the object.
  • Supported The object is placed in the transaction of its creator if one exists. If the creator isn't running in a transaction, the object is created without a transaction.
  • Not Supported The object is never created in a transaction.

An external client can initiate a transaction by activating an object from a configured component marked as Required or Requires New. The COM+ runtime determines that the new object must run in a transaction and that the creator isn't running in a transaction. The COM+ runtime therefore creates a new COM+ transaction and then creates the new object in it.

The first object created in a transaction, which is known as the transaction's root object, has an important role. It can create additional secondary objects in its transaction. If you're writing a method implementation for a COM+ object and you want to propagate another object in that transaction, you must create the object from components marked as Required or Supported.

You should note a subtle difference between Required and Requires New. A component marked as Required can be used to create either root objects or secondary objects. A component marked as Requires New can be used only to create root objects. As you learn more about the roles of the root object and secondary objects, you'll begin to appreciate the difference.

It's common to create COM+ transactions that contain multiple objects. However, let's first examine a transaction that contains only one object—the root object. Once you know exactly how the root object works, it'll be easier to understand what happens when you have several objects running in a transaction at once.

A tale of two transactions

When a client creates an object from a configured component marked as Required, the COM+ runtime creates a new COM+ transaction in which to place the new object. At some point the COM+ runtime also calls down to the DTC to create a new distributed transaction. You should think of the COM+ transaction as the logical transaction and the DTC-based transaction as the physical transaction. Note that the COM+ runtime doesn't create the physical transaction when it creates the logical transaction.

The COM+ runtime creates the logical transaction when it creates the root object, but it defers creating the physical transaction until the root object starts its work. Thus, a delay occurs between the creation of the logical transaction and creation of the physical transaction. The COM+ runtime delays creating the physical transaction as long as possible as an optimization to keep transaction times as short as possible.

In Chapter 7, I introduced the concept of just-in-time activation, which is extremely important to your understanding of declarative transactions. Here's a quick recap. The client never directly holds an object that supports just-in-time activation. Instead, the client holds onto a system-provided proxy that allows the COM+ runtime to play a special trick in the interception layer. The COM+ runtime activates an object at the beginning of each call and deactivates it before returning control to the client. The client can't tell what's going on, but each method call is serviced by a new and different object (assuming your transactional objects are not poolable).

A delay always occurs between the time the client creates an object and the time it issues the first method call. In MTS, the runtime defers starting the DTC-based transaction until the root object is activated at the first method call. COM+ adds a new optimization: The COM+ runtime doesn't start the DTC-based transaction until the root object makes its first "interesting" call. An interesting call is loosely defined as a call that requires the COM+ runtime to start the DTC-based transaction. An example of an interesting call is one that establishes a database connection that needs to be auto-enlisted or that queries for the TransactionID. In both cases, the COM+ runtime must call into the DTC and start a distributed transaction to complete its work.

When the COM+ runtime determines that it's time to start the distributed transaction, it issues a BeginTransaction call to the DTC. The COM+ runtime creates the physical transaction with an isolation level of Serializable and a default timeout interval of 60 seconds. This means that the transaction runs at the highest level of isolation and that you have one minute to complete your work.

This timeout interval is adjustable at the machine level as well as the component level. It's important to keep timeout intervals short because they help in the detection and resolution of deadlocks. (I'll talk about issues related to deadlocks later in the chapter.)

When the COM+ runtime calls down to create the physical transaction, the DTC creates a transaction object and passes the COM+ runtime an ITransaction reference. The COM+ runtime holds onto this reference for the lifetime of the logical COM+ transaction. The ITransaction interface lets the COM+ runtime call Commit or Abort. As you can see, the COM+ runtime has the ability to control the outcome of the transaction.

As a Visual Basic programmer, you shouldn't obtain an ITransaction reference to the transaction object and call Commit or Abort yourself. You're probably wondering, "So how do I control the transaction?" You have to know when the COM+ runtime calls Commit and Abort and determine how you can influence its decision.

Everything about declarative transactions is based on one major assumption: The COM+ runtime decides whether to commit or abort the transaction when the root object is deactivated. Before it makes this decision, you can do quite a lot to change the state of the transaction. You can invoke system-supplied methods that allow each object to vote on whether the transaction should succeed.

Three Important Flags: Happy, Done, and Doomed

Figure 8-7 shows a diagram of a client, a transaction, a root object, and the root object's context. It also shows some important internal flags that COM+ maintains. These flags are simply system-defined variables that have a value of True or False. For this reason, they're called bits. You should learn how to modify these bits and understand how they influence the COM+ runtime's decision to commit or abort a transaction.

click to view at full size.

Figure 8-7 The happy bit holds the object's vote on whether the transaction should be committed. The COM+ runtime knows when to deactivate the object by inspecting the done bit.

The COM+ runtime lets each transactional object vote on whether the transaction should be committed or rolled back. The object's vote is stored in a context-specific area known as the happy bit. (Programmers without a sense of humor can call it the consistency bit.) This Boolean flag simply tells the COM+ runtime whether the object is willing to commit whatever work it's done. The default value for the happy bit is True.

The other important context-specific flag of an object in a transaction is the done bit, which allows an object to tell the COM+ runtime that it has completed its work. If the root object sets its done bit to True, it triggers its own deactivation. The motivation for doing this is to release the transaction as soon as possible. The default value of the done bit is False.

There's one other important flag, called the doomed bit. This is a single flag that is shared by every object and context in the transaction. This flag is initially set to False when a COM+ transaction is created. This flag is important because the COM+ runtime uses it when deciding whether to commit or abort the transaction.

When does the COM+ runtime inspect the doomed bit? And how can you change the value of the doomed bit? To answer these questions, you must understand the important role that a root object plays in every COM+ transaction. The COM+ runtime inspects the doomed bit when the root object is deactivated. If the root object is deactivated during an active transaction, the COM+ runtime inspects the doomed bit and releases the transaction by calling Commit or Abort. If the doomed bit is set to False, the COM+ runtime calls Commit. If the doomed bit is set to True, the COM+ runtime calls Abort.

The deactivation of the root object should always cause the end of the transaction's life cycle. When the root is deactivated, the transaction is released. As long as the root object remains activated, the transaction can remain alive and can hold all of its locks.

Now let's look at the two flags maintained in the context of every transactional object. The first one is the happy bit, which has an initial value of True. When an object running in a COM+ transaction is deactivated, the COM+ runtime examines its happy bit. If the happy bit is set to False, the COM+ runtime sets the transaction's doomed bit to True. Once the transaction's doomed bit is set to True, it can't be reversed. This has a powerful implication. If the root object or any secondary object in a transaction is deactivated in an unhappy state, the transaction is doomed to failure.

Let's look at a few scenarios to see how all this works. First, imagine that a client creates an object from a component marked as Required and invokes a method call that opens a connection to the DBMS and inserts a record. This results in the creation of a COM+ transaction and triggers the COM+ runtime to call down to the DTC to create the physical transaction as well. At this point, what happens if the client simply releases its reference to the root object? When the root object is deactivated, the COM+ runtime inspects its happy bit. The happy bit still has its initial value of True. Therefore, the COM+ runtime doesn't change the doomed bit. The doomed bit remains False, and the COM+ runtime calls Commit on the transaction. You can run a simple example and confirm these results by examining the Transaction Statistics in the Component Services administrative tool.

So that's pretty easy. You create an object in a transaction, call a method that does some work, and then release the object. It takes only three steps to successfully begin and commit a transaction with the DTC. This example shows how and when the COM+ runtime interacts with the DTC. We didn't write any code to explicitly begin or commit the transaction because the COM+ logical transaction does all of that for you.

Now let's write some code to force the COM+ runtime to roll back a transaction. All you do is set the root object's happy bit to False. One way to do this is by calling DisableCommit in a method with the following code:

 GetObjectContext.DisableCommit 

When the client invokes a method on the root object with this code, the COM+ runtime changes the value of the happy bit to False. Now, when the client releases its connection, the root object is deactivated. During the object's deactivation, the COM+ runtime sees that the happy bit is False and changes the value of the transaction's doomed bit to True. When the root object is deactivated, the COM+ runtime calls Abort on the transaction.

DisableCommit is complemented by another method named EnableCommit, which simply returns the happy bit to True. You can call each of these methods repeatedly. The happy bit is just a Boolean value, so whichever method is called last before the object is deactivated determines how the COM+ runtime handles the transaction. When you call one of these methods, you're simply voting on whether the transaction should succeed. You can call EnableCommit and DisableCommit as many times as you like within a given method. Only the last call before the object's deactivation matters.

The SetComplete and SetAbort Methods

In addition to DisableCommit and EnableCommit, the ObjectContext interface contains two other important methods for controlling a transaction: SetComplete and SetAbort. Like the other two methods, these cast a vote by modifying the happy bit. SetComplete sets the happy bit to True, and SetAbort sets it to False. However, SetComplete and SetAbort are different from the other two methods because they set the done bit to True. As you'll recall from Chapter 7, the done bit has a dramatic effect on an object's life cycle. DisableCommit and EnableCommit modify the done bit as well, but they set the done bit to False, which usually has no effect because it's the default value.

After the root object finishes executing a method call, it passes control back to the interception layer. This gives the COM+ runtime an opportunity to inspect the done bit before returning control to the client. If the done bit is set to True, the COM+ runtime deactivates the object. This is extremely important because it speeds up the release of the transaction. The root object can thus proactively release a transaction by calling either SetComplete or SetAbort and then returning control back to the client.

If an object that's the root of a transaction calls SetAbort, it has the effect of dooming the transaction. The COM+ runtime deactivates the object in an unhappy state, sets the doomed bit to true and aborts the transaction. If an object that's the root of a transaction calls SetComplete instead, the COM+ runtime deactivates the object and attempts to commit the transaction immediately. The COM+ runtime still inspects all the happy bits to decide whether to commit or abort the transaction in the same way that it does when you call EnableCommit and DisableCommit. SetComplete and SetAbort simply force the COM+ runtime to end the transaction much faster, which means that those expensive locks that your transaction was holding are released earlier. You don't have to wait for the client to release the root object.

You need to use SetComplete and SetAbort at the appropriate times. If you cast your vote by calling the DisableCommit or EnableCommit method in the root object, the transaction and all of its locks are held until the client releases the root object. Calling the SetComplete or SetAbort method is a much better approach because the root object forces the COM+ runtime to release the transaction. As you know, the most important thing you can do to improve concurrency and throughput in an OLTP environment is to reduce the amount of time that any transaction holds its locks.

You've seen that the COM+ runtime deactivates the root object and releases the transaction when you call SetComplete or SetAbort. This leads to another important question: How does the deactivation of the root object affect the client? When a client invokes a method that includes a call to SetComplete or SetAbort, the object is deactivated and destroyed. If the client had to deal with the fact that the object has died, you'd have a messy problem. Fortunately, the just-in-time activation scheme can hide the object's demise from the client.

If the client continues to call methods that call SetComplete or SetAbort, the COM+ runtime creates and releases a new root object and a new transaction each time. COM+ provides just-in-time activation followed by as-soon-as-possible deactivation. All of this creating and releasing occurs within the window of time that starts with a method call's preprocessing and ends with its post-processing. A COM+ transaction and the objects in it should be flashes in the pan. In the world of OLTP, shorter transactions result in better concurrency and throughput.

The IContextState Interface

So far, I've described four methods of the ObjectContext interface that allow you to control a transaction. COM+ introduces a new interface named IContextState that has similar functionality. Here are the four methods of this interface:

  • SetMyTransactionVote Sets the happy bit to True or False.
  • GetMyTransactionVote Reads the state of the happy bit.
  • SetDeactivateOnReturn Sets the done bit to True or False.
  • GetDeactivateOnReturn Reads the state of the done bit.

You should see that these methods allow you to change the happy and done bits just as the methods in the ObjectContext interface do. For example, you can replace a call to SetComplete with a call to SetMyTransactionVote and SetDeactivateOnReturn. As long as you know how to set the happy bit and done bit to True, it doesn't really matter how you do it.

So what's the difference between using the ObjectContext interface and the IContextState interface to control a transaction? First, IContextState let's you control the happy and done bits individually. Second, IContextState allows you to read the current state of those bits, unlike the ObjectContext interface. The third difference is that the COM+ runtime raises errors when you call an IContextState method on an object that isn't configured properly. Calls to GetMyTransactionVote and SetMyTransactionVote fail if the object isn't running in a transaction. Calls to SetDeactivateOnReturn and GetDeactivateOnReturn fail if the object doesn't support just-in-time activation.

So how do you choose between the ObjectContext interface and the IContextState interface when you write a transactional component? The truth is that it doesn't really matter much. Programmers who've been using MTS might prefer calling SetComplete and SetAbort because they're familiar with that approach. Other programmers might prefer IContextState because they want to read the state of the happy bit and the done bit. C++ programmers who are creating poolable objects that aren't involved in transactions should use IContextState because they'll want to manipulate the done bit without touching the happy bit.

As long as you know how the COM+ runtime behaves and you know how to manipulate the happy bit and the done bit, you can use either interface. In the rest of this chapter, I'll use the methods in the ObjectContext interface. You should simply note that you can use the methods of IContextState interchangeably.

The AutoComplete Attribute

One of the most important things to remember when you create the component for a root object is to release every transaction as soon as possible. The most common way to do this is by setting the done bit to True with a call to SetComplete or SetAbort. These calls are required because the default value of the done bit is False.

Each method of a configured component has an AutoComplete attribute that you can use to change the default value of the done bit to True. You can configure this attribute from the method's Properties dialog box in the Component Services administrative tool by selecting the Automatically Deactivate This Object When This Method Returns check box. This attribute sets the default behavior of a root object so that it deactivates automatically even if you don't include a call to SetComplete or SetAbort. This attribute also automatically sets the happy bit to False when the method raises an error back to its caller.

This feature is especially valuable for prewritten components that don't include calls to SetComplete or SetAbort. Perhaps you'll want to use such a component as the root of a transaction without having to modify any code. You can configure each method for AutoComplete to ensure that each method will start and release a transaction.

If you're writing a transactional component, the use of the AutoComplete attribute is optional. It offers convenience, but you'll have more control if you manipulate the happy bit and the done bit yourself. Moreover, it's important to remember that although the AutoComplete attribute changes the default value of the done bit to True, it doesn't guarantee that the done bit will remain True. You have to watch out because calls to EnableCommit, DisableCommit, and SetDeactivateOnReturn can set the done bit back to False and keep a transaction alive longer than you want.

The ObjectControl interface

The COM+ runtime manages the life cycle of every object that's created from a configured component. The life cycle of an object that supports just-in-time activation includes four stages: creation, activation, deactivation, and destruction. When you're programming COM+ transactions, you should be aware of when the transitions between these stages occur so that you can take appropriate action.

A Visual Basic class module provides an Initialize procedure. The code you write in Initialize is guaranteed to run when the Visual Basic object is created. However, Initialize always runs before the object has been activated. If you wait until after the object has been activated, the contextual information supplied by the COM+ runtime is richer and more reliable.

The Terminate procedure in a Visual Basic class is executed prior to the object's destruction yet after the object has been deactivated. Terminate is similar to Initialize in that it's fired when the object isn't in an active state. Neither Initialize nor Terminate gives you the control you need to manage your object's life cycle inside a COM+ transaction. Fortunately, the COM+ runtime can help by notifying your object just after activation and once again just before deactivation.

Here's how it works. The COM+ Services type library includes a definition for an interface named ObjectControl. When the COM+ runtime creates an object with support for just-in-time activation, it calls QueryInterface to determine whether the object supports this interface. If the object implements ObjectControl, the COM+ runtime calls methods in this interface to notify the object at important transition stages during its life cycle. This means that you should implement ObjectControl in every object that you think needs to receive these notifications. The ObjectControl interface contains the following three methods.

  • Activate This method is called by the COM+ runtime after activation and just before the first method call is executed.
  • Deactivate This method is called by the COM+ runtime just before the object is deactivated.
  • CanBePooled For components that support object pooling, this method allows an object to tell the COM+ runtime whether it should be destroyed or placed in the pool. It's called by the COM+ runtime after deactivation. When you implement the ObjectControl interface in a Visual Basic component, an implementation of this method is required and meaningless.

So that your object receives these notifications, you should implement the ObjectControl interface in the MultiUse class modules in your ActiveX DLLs. Here's an example of a Visual Basic class module that implements this interface:

 Implements ObjectControl Private Sub ObjectControl_Activate()     ' Your code for initialization after activation End Sub Private Sub ObjectControl_Deactivate()     ' Your code for cleanup before deactivation End Sub Private Function ObjectControl_CanBePooled() As Boolean     ' Object pooling not supported for Visual Basic components     ObjectControl_CanBePooled = False End Function 

The COM+ runtime calls Activate just before the execution of the first method call. However, the Activate method executes while the object is in a fully active state. If your component is going to be configured for just-in-time activation, it's usually best to put your object initialization code inside Activate instead of Initialize. Likewise, you should place your cleanup code in the Deactivate method instead of Terminate.

Remember there are only two types of components that need just-in-time activation. Poolable components rely on just-in-time activation in order to release objects back to the pool in a more efficient manner. Components that run in COM+ transactions rely on just-in-time activation to keep transaction times short.

If you're creating a component that isn't poolable and that doesn't support transactions, you don't really need just-in-time activation. It's a small optimization to disable just-in-time activation for components that don't need it. Remember that the ObjectControl interface is just for objects that support just-in-time activation. If you have disabled just-in-time activation, you should use Visual Basic's Initialize and Terminate procedures for initialization and cleanup code.

Multiobject Transactions

As you know, the root object can create secondary objects in the same transaction by calling CreateObject on components that are marked as Required or Supported. (I'm sure you're well aware of this by now, but let me reemphasize that calling New on a class compiled into the same DLL causes serious problems in COM+.) Figure 8-8 shows a root object and two secondary objects in a COM+ transaction. Each object has its own context and its own happy bit and done bit.

click to view at full size.

Figure 8-8 Multiobject transactions always have a root. Secondary objects play a different role than the root object.

A COM+ transaction is a democratic community in which each object gets to vote on whether the transaction should succeed. A secondary object follows the same rules as the root object. When a secondary object is deactivated, the COM+ runtime inspects its happy bit. If the happy bit is set to False, the COM+ runtime sets the transaction's doomed bit to True, which dooms the transaction to failure. There's nothing that any other object can do to set the doomed bit back to False. This means that any object in a transaction can prevent the transaction from committing.

It's common to have a design in which the root object creates secondary objects and uses them to complete the work for the transaction. If you plan to create multiobject transactions, you must understand how all the objects work together so that you can coordinate communication among them. More specifically, you should understand the responsibilities of secondary objects as well as the root object. Secondary objects play a much different role than the root object.

Let's look at the case in which the root object makes several successful method calls on a secondary object. As long as the secondary object doesn't set its done bit to True, it remains alive until it's released by the root object. The root object can make several successful calls on the secondary object and then call SetComplete to commit the transaction. When the root calls SetComplete, both objects are deactivated. The secondary object is deactivated first, followed by the root object. As long as neither object sets its happy bit to False by calling SetAbort or DisableCommit, the transaction is committed.

If a secondary object doesn't explicitly vote in the transaction outcome, it gives the COM+ runtime passive consent to commit the transaction because its happy bit is set to True by default. Also note that a secondary object doesn't need to set its done bit to True. This means that calling SetComplete or SetAbort in a secondary object is optional. This is quite different from a root object where calls to SetComplete or SetAbort are critical to releasing the transaction.

What happens if a secondary object wants to roll back a transaction? If a secondary object calls DisableCommit, it sets its happy bit to False. Now, the root object has a delicate situation on its hands. If the secondary object is deactivated in an unhappy state, it will doom the transaction. At this point, the root object has two choices. First, it can attempt to call another method on the secondary object and try to persuade it to change its happy bit to True. If this isn't possible, the second choice is for the root object to admit defeat. In this case, it should call SetAbort and raise an error back to the client.

One really important point is that the root should always be aware when a secondary object is unhappy. If the root object calls SetComplete in a transaction in which one or more secondary objects is unhappy, the COM+ runtime sees the conflict. The root object is trying to commit a transaction that must be rolled back. The COM+ runtime deals with this situation by deactivating all the objects and raising an mtsErrCtxAborted error back to the client.

Instead of calling DisableCommit, a secondary object can force a rollback by calling SetAbort. This has an interesting effect because the secondary object gets deactivated and dooms the transaction before control is returned to the root object. Now the root object is running in a doomed transaction. If the root object tries to activate another object in a doomed transaction, it experiences an mtsErrCtxAborting error. If the root object simply returns control to its caller without calling SetAbort, it's left activated in a crippled state. Future method calls on the root object will probably result in mtsErrCtxAborted and mtsErrCtxAborting errors being raised by the COM+ runtime.

These problems should lead you to one conclusion: When a secondary object wants to roll back a transaction by calling DisableCommit or SetAbort, the root object should also call SetAbort and halt any attempt to complete additional work. There's one small hitch: The root object can't examine the doomed bit. It can't ask the COM+ runtime whether a secondary object is unhappy. Therefore, your secondary objects should proactively communicate with the root object when they call DisableCommit or SetAbort. You can use the return value or output parameters in the methods of your secondary objects to indicate whether they're unhappy, or you can raise errors from the secondary objects back to the root object.

For example, if a secondary object decides to roll back the entire transaction in Method1, it can use the following sequence of calls:

 GetObjectContext.DisableCommit Dim ErrorCode As Long, Description As String ErrorCode = myErrorEnum1 ' Something like (vbObjectError + 512) Description = "The requested quantity is not available." Err.Raise ErrorCode, , Description 

If you follow this convention, a method implementation in the root object can assume that every secondary objects raises an error after setting its happy bit to False. This means that an error handler in the root object should call SetAbort and raise its own error to forward the secondary object's error description back to the client. If the root object can call methods on the secondary objects without experiencing an error, it can assume that everything is fine and call SetComplete.

Here's a typical method in the root object. Notice that all paths of execution result in a call to SetComplete or SetAbort.

 Sub RootMethod1()     On Error GoTo MyHandler     Dim Secondary1 As CSecondary     Set Secondary1 = CreateObject("MyDll.CSecondary")     Secondary1.Method1     Secondary1.Method2     ' Commit transaction if all calls complete successfully.     GetObjectContext.SetComplete Exit Sub MyHandler:     ' Roll back transaction and get out ASAP on error.     GetObjectContext.SetAbort     ' Forward error information back to base client.     Err.Raise Err.Number, , Err.Description End Sub 

Of course, this code shows only one of many possible approaches. If you take a different approach, you must carefully coordinate the communication between the secondary objects and the root object to follow these rules: Always call SetComplete or SetAbort in the root object, and never call SetComplete in the root object when a secondary object is unhappy.

You should keep in mind a few other important issues. First, you should be aware of a bug in the initial release of COM+ and Windows 2000 that prevents secondary objects from passing error descriptions to the root object after calling SetAbort. (See the sidebar below titled "Watch Out for the SetAbort Bug." This means that you might prefer using DisableCommit over SetAbort. The second thing to keep in mind is that a call to DisableCommit in a secondary object doesn't necessarily doom the transaction.

Watch Out for the SetAbort Bug

Unfortunately, a bug in the initial release of COM+ can occur when a secondary object calls SetAbort and raises an error back to the root object. This bug is in the lightweight proxy that COM+ builds to connect the root object to a secondary object in the same STA. The bug is specific to Visual Basic components.

The bug overwrites any custom error description you're trying to propagate from the secondary object back to the root and replaces it with the dreaded description method '~' of object '~' failed. The workaround for this bug is to avoid calling SetAbort or SetDeactivateOnReturn(True) in secondary objects that raise errors. To roll back a transaction, a secondary object should call DisableCommit or SetMyTransactionVote(txAbort) instead. As long as the secondary object doesn't set its done bit, it can raise an error with a reliable description back to the root and still vote to roll back the transaction.

This bug raises two concerns. First, a lot of code written for MTS has secondary objects that call SetAbort. This bug doesn't exist in MTS, so many developers are in the habit of calling SetAbort and raising an error when a problem is encountered. This means that you might have to touch up MTS code when you port it to COM+.

The second problem is that a component must be written to be either a root object or a secondary object. The root object should always call SetComplete or SetAbort. But this is a catch-22 because secondary objects should never call SetAbort. The bug prevents you from writing a component that can be used interchangeably as the root object or a secondary object.

A fix for this bug will appear in the first service pack for COM+. Once this bug has been removed from COM+, you'll be able to port your MTS code more easily and write a component that can be used as either the root object or a secondary object.

If a secondary object calls DisableCommit and returns control to the root, it has indicated that it can't commit its work in its present state. However, DisableCommit doesn't deactivate the secondary object upon return to the root object. This is different from a call to SetAbort, in which the COM+ runtime deactivates the secondary object before the root object regains control. A call to SetAbort dooms the transaction to failure. When a secondary object calls DisableCommit, it says, "I am currently unhappy, but perhaps the root object can invoke another method and make me change my mind."

As you might imagine, using DisableCommit in this manner requires you to design a more elaborate communication protocol among the objects in a transaction. When a secondary object calls DisableCommit, the root object can try to persuade the object to change its mind by executing additional methods. However, the root object must ultimately call SetComplete or SetAbort. Therefore, the root object must find a way to make the secondary object happy or determine that the transaction can't be saved.

You should definitely avoid calling DisableCommit from the root object. You don't want to pass control back to the client when a transaction is pending. Never allow the client to control when the locks are released. This usually results in locks being left on data items for longer than necessary. This conflicts with your newfound "get in and get out" mindset. OK, enough said.

Database Connections and Auto-Enlistment

Now that we've covered the basics of how to commit and roll back a COM+ transaction, let's discuss how your database connections get enlisted. When you connect to an RM, the connection must be enlisted with the DTC, as you can see in Figure 8-9. This involves setting up a line of communication between the RM and one or more sessions of the DTC. As you saw earlier in this chapter, these lines of communication are used to execute the two-phase commit protocol.

Enlisting a connection in a COM+ application is actually quite easy. You simply have to follow two rules. First, you must establish the connection from an object running in a transaction. Second, you must be sure that you're using an RM that works with the DTC and that you're using an RM proxy that supports auto-enlistment. For example, SQL Server is an RM that works with the DTC, and the native OLE-DB provider for SQL Server is an RM proxy that supports auto-enlistment.

When you establish a typical connection from an object in a transaction using ADO, the RM proxy detects the need for auto-enlistment and makes all the required calls for enlistment behind the scenes. You open a database connection in the usual manner, and your write and read operations are automatically part of a distributed transaction. It couldn't be easier.

click to view at full size.

Figure 8-9 When you create a connection to an RM such as SQL Server or Oracle from an object in a COM+ transaction, the COM+ runtime and the RM proxy automatically enlist the connection with the DTC.

Let's look at an example. If you're working with SQL Server 7 and connecting through ADO with the native OLE-DB provider, you can connect using this code:

 Dim conn As ADODB.Connection Set conn = New ADODB.Connection conn.Open MyConnectiString 

As you can see, this is the same code that you'd write to establish any ADO connection. The COM+ runtime works with the native OLE-DB provider to auto-enlist any ADO connection made from inside a COM+ transaction. The RM proxy interacts with the DBMS to set up a communication channel with the coordinating DTC, as shown in Figure 8-9. You simply make your connections and begin accessing data. All of your changes are charged against a single distributed transaction that's controlled by the COM+ runtime.

In Chapter 7, I covered the basics of database connection pooling. In addition to standard database connection pooling, COM+ also allows for the pooling of enlisted connections. As you've seen, when a connection is established, it requires a round trip to the DBMS. Likewise, when connections are enlisted in a distributed transaction, they require even more round trips to the DBMS. The pooling of enlisted connections is a further optimization to reduce round trips.

When you close an enlisted connection from an object running in a transaction, COM+ returns it to a special pool. Most of the time, this pool won't contain more than one physical connection. If another object in the same transaction requests a connection using the same connect string, COM+ simply reuses the same enlisted connection, which means that several objects in a transaction can use the same enlisted connection. This scheme prevents redundant round trips to the same DBMS for enlistment to the same transaction. The COM+ runtime takes care of cleaning up the enlisted connection when the transaction is released.

COM+ Transactions and Stored Procedures with Transactions

When programmers want to call stored procedures from an object in a COM+ transaction, they often wonder what will happen if the stored procedure contains calls to control a local transaction, such as BEGIN TRAN, COMMIT TRAN, or ROLLBACK TRAN. How will these calls affect the behavior of the code when they're already running in a distributed transaction? The answer is simpler than you might suspect. Calls to BEGIN TRAN and COMMIT TRAN are ignored, while calls to ROLLBACK TRAN are honored.

A call to BEGIN TRAN is ignored because the transaction is already started. Furthermore, neither the DTC nor SQL Server supports the concept of nested transactions. A call to BEGIN TRAN must be ignored because it's impossible to start what's already been started.

It's also important to see that the stored procedure can't commit a distributed transaction if it didn't start the transaction. The stored procedure is just like a secondary object. It can vote to roll back the transaction, but it can't be the one that ultimately commits it. This means that calls to COMMIT TRAN are ignored as well.

If a stored procedure calls ROLLBACK TRAN, it dooms the transaction. In this sense, a stored procedure that calls ROLLBACK TRAN is just like a secondary object that calls SetAbort or DisableCommit. A root object will experience problems if it calls SetComplete after a stored procedure has rolled back the transaction. Therefore, it's important for a stored procedure to raise an error indicating its intention to roll back a transaction. If your stored procedure raises errors in this fashion, you can handle transaction rollback in a graceful manner inside the root object.

The Short, Happy Life of a Transactional Object

I've been building on a theme throughout this chapter: To reach higher levels of concurrency and throughput, you must release your locks as quickly as possible. When you're using transactional objects, the locks you acquire are held until you deactivate the root object. You should deactivate the root object as soon as possible by calling a method such as SetComplete or SetAbort. However, this aspect of the programming model can seem strange at first. COM+ must destroy all the objects associated with the transaction as part of the cleanup process. The root object and any secondary objects live for only the duration of a single method call.

This programming model is much different from that of classic object-oriented programming (OOP) and of COM, and it requires a new way of thinking. The OOP and COM paradigms fail to address scenarios in which state is discarded at the completion of each method call. Object-oriented clients assume that they can obtain a reference to a long-lived object. If a client modifies some state within an object, object-oriented programmers assume that the object will hold these changes across multiple method calls. But this isn't the case with transactional objects in COM+. Each object must die as part of the transaction's cleanup, and its state must go along with it.

This transparent destruction of transactional objects is often called stateless programming. In the short history of MTS and COM+, there's been a good deal of confusion about why statelessness is an essential aspect of programming transactions. Many people have suggested that stateless programming is about reclaiming memory on the computer running the COM+ application. They argue that destroying objects and reclaiming memory results in higher levels of scalability because of more efficient resource usage. Their argument is both confusing and inaccurate.

As a Visual Basic programmer, your only reason for setting the done bit to True is to ensure the semantics of a transaction. The idea is that a transactional object can see a data item in a consistent state only while the RM is holding a lock. If a COM+ object holds a copy of this data item after the lock has been released, another transaction can modify the original data item inside the RM. The original data item and the copy can thus get out of sync and violate the ACID rules of a transaction. The point of destroying objects at transactional boundaries is that any copy of a data item must be thrown away when the RM releases its locks. This is why COM+ requires that all transactional objects be destroyed when a transaction is released.

If you have an object that isn't involved in a transaction, holding state across method calls doesn't cause any problem. If you set a component's transaction support property to Not Supported and you don't call SetComplete or SetAbort, you can create stateful objects.

One strange aspect of programming declarative transactions is that an object dies before its transaction is released. This means that an object never truly knows the outcome of the transaction in which it was running. Even the root object is destroyed before the COM+ runtime starts running the two-phase commit protocol. This can bring up a few interesting issues when you're designing a transactional application.

Let's say you have a transaction with a root object and a secondary object. When the root is activated, it creates the secondary object and calls a method to perform some work. Assume that both objects are happy. When the root object calls SetComplete and returns, the COM+ runtime deactivates both objects and starts running the two-phase commit protocol.

What happens if the COM+ runtime experiences a failure in phase 1? It can't raise an error to the root object because the root object has already been destroyed. The only thing it can do in this case is to raise an mtsErrCtxAborted error back to the client. However, if your client is a forms-based Visual Basic application or an ASP page, it's not very elegant to send it an error code that's defined in the COM+ Services Type Library. It can often be helpful to add another nontransactional component to your design to deal with this error. This new component plays the role of a transaction observer.

Figure 8-10 shows two different clients running transactions. Client 1 is connected directly to the root object, while client 2 is indirectly connected to the root object through a nontransactional object. One of the biggest benefits of the latter design is that the client can be shielded from mtsErrCtxAborted errors that can occur during phase 1 of the two-phase commit protocol. The client invokes a method on the nontransactional object; the nontransactional object then invokes a method on the root object to run a transaction. The nontransactional object can observe the outcome of the transaction and can ultimately determine whether the transaction was committed or rolled back.

click to view at full size.

Figure 8-10 Placing a nontransactional object between the root and the client can offer many benefits.

A nontransactional object can also maintain client-specific state across multiple method calls. This can be advantageous in certain designs because a stateful object can reduce the need to pass parameters and reinitialize objects.

I'm not suggesting that you can always use stateful objects. In many situations (especially with Web-based applications), your objects live for the duration of a single client request, whether or not they're transactional. However, in a few places in distributed application design, stateful objects offer the best approach. When you encounter a situation in which it makes sense to keep a stateful object alive across a series of calls, be sure that the object isn't transactional and don't set the done bit to True—and remember, there's no good reason to call SetComplete from a Visual Basic object that's not running in a transaction.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

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