Chapter 8. Transactions

A universe that is clearly guided by the relationship between cause and effect is often referred to as Newtonian. Your enterprise-scale application seems to live in a predictable, Newtonian universe, but every time it wants to make a permanent record of its state, it enters a period of uncertainty. In physics, this period of uncertainty is described by the laws of quantum mechanics; in development, it is the law of transactions.

Most large applications and IT infrastructures keep their state inrelational database management systems (RDBMSs). Microsoft SQL Server, Sybase, Oracle, Informix, and Ingres are all examples of RDBMSs. All these systems are transactional, meaning that sequences of data modifications succeed or fail together as an atomic group allowing no chance for inconsistent, partially updated data to exist in the system. RDBMS architects employ a transactional model because they realize that there is a certain chance that one or many related modifications could fail; and should that happen, the entire series of modifications must be undone.

Grouping one or many data modifications into an atomic unit of work is the hallmark of what is called a transaction. What goes on inside a transaction as all the possibilities are explored (and the dice of chance is rolled) is not viewable to the outside world. If all goes well, the transaction ultimately collapses into a codified, singular result as the data modifications are committed to permanent record. If things do not go well, all intermediate changes are undone and the transaction evaporates into nothingness—that is, rollback affecting no permanent record.

COM+ Performance: Top Rated According to TPC-C Benchmarks

While developers should always strive to write efficient components, it is good to know that COM+ itself is a top performer. At the time of this writing, COM+ claims the number one spot on the Transaction Processing Council's (http://www.tpc.org) TPC-C benchmark. According to this benchmark, when run on an IBM Netfinity 8500R c/s with DB2 7.1 and Advanced Server, COM+ is able to originate 440,879.95 tpmC (transactions per minute). The TPC-C is a scenario-based test that simulates a high-traffic order entry system. 440,879.95 tpmC means that COM+ was able to process 440,879.95 new order requests while the system was simultaneously processing other requests. To quote TPC's own FAQ, "Throughput, in TPC terms, is a measure of maximum sustained system performance. In TPC-C, throughput is defined as how many New-Order transactions per minute a system generates while the system is executing four other transactions types (Payment, Order-Status, Delivery, Stock-Level). All five TPC-C transactions have a certain user response time requirement, with the New-Order transaction response time set at 5 seconds. Therefore, for a 710 tpmC number, a system is generating 710 New-Order transactions per minute while fulfilling the rest of the TPC-C transaction mix workload."

Although the top performance slot seemingly relies on an all-IBM platform, COM+ claims the top six positions when ranked according to price and performance. That is, according to the TPC, when you look at the total cost of the system, your best performance for the price comes in the form of a Dell, Compaq, or Hewlett Packard running an all Microsoft backbone: SQL Server 7 or 2000, Windows 2000 or (NT 4 Enterprise Edition), and COM+ (or Microsoft Transaction Server [MTS]).

Certainly, one should keep in mind that all benchmarks have arguable points. The components used in this test are certainly highly optimized, and the cost of development is not taken into account. Similarly, system reliability is not considered. Nonetheless, this metric correlates well to the industry perception; for years UNIX/Oracle systems running the BEA Tuxedo transaction processing monitor have dominated. However, COM+ has made a resoundingly strong entrance, beating out its closest performance rivals by a factor of three. If you are using COM+, feel good about it; you are not giving up anything for ease of use and development. And if you are not using COM+ and shopping around for the appropriate transaction processing system, know that Microsoft is a real player in this arena.

To find out more about the TPC-C benchmark, be sure to go to http://www.tpc.org.

ROLLBACK and COMMIT

If your life supported transactions as a relational database does, you could immediately remove yourself from any embarrassing, unexpected, or dangerous situation arising from your actions. In fact, all you would need to do is shout the word "ROLLBACK" at any time, in any situation you didn't plan (or want) to be in, and you would immediately be whisked back in time to a safe point before the unpleasant situation arose.

Of course, the question arises: What would this safe point in time be? Well, if you know you are just about to embark on a series of actions that might not turn out in a way you'd like, you first shout the words "BEGIN TRANSACTION." This establishes your rollback point—that is, the point in time to which you want to return in the event your actions don't produce the result you want or expect.

In the previous example, we discussed BEGIN TRANSACTION and ROLLBACK, but you're missing one other key component—COMMIT. BEGIN TRANSACTION is used to specify a safe place in time where everything is okay or, in database terminology, consistent. As discussed earlier, after BEGIN TRANSACTION, a ROLLBACK can always be called and it undoes all actions occurring after the BEGIN TRANSACTION, bringing you back to your safe or consistent point.

However, suppose things go well for you in your transaction-enabled life, and all your actions produce the desired results. But, way back in your history, there is a dangling BEGIN TRANSACTION—for which there was no COMMIT.

Assuming that this is an unappealing prospect, the time soon comes when you should really commit to the actions you've executed, close out that BEGIN TRANSACTION, and make everything permanent. You do this simply by uttering the word "COMMIT." That command closes out the transaction and makes all your actions permanent; you can never roll them back.

Classical Transactions and Traditional Databases

Sadly, you acknowledge that you cannot have transactions in real life. Your database systems, however, are more fortunate. And although they might not be especially concerned with rolling back to prevent from being embarrassed or making a bad life decision, they are deeply concerned with ACID. ACID is an acronym representing the following four guiding principles of transactions:

  • Atomicity

  • Consistency

  • Isolation

  • Durability

Atomicity

Transactions need to be atomic. There was a time in classical physics when atoms were thought to be the absolute smallest constituents of matter, and the term atomic meant all or nothing. To a database, this means that if a sequence of data modification statements are made within the context of a transaction (that is, they occur between a BEGIN TRANSACTION and a COMMIT TRANSACTION statement), they either all succeed or none of them succeed. If four of the five modification statements succeed but the fifth one doesn't, the four that did succeed are rolled back and the database is returned to the consistent state it was in at the time BEGIN TRANSACTION was declared.

Consistency

No matter what happens in a transaction, data in the database should remain accessible, any partial changes should be rolled back, and any internal data structures the database might use to keep track of the data (memory-cached indexes, for example) must be correct.

Isolation

Today, we know that the atom is no longer truly atomic—there are smaller elements of matter. Quantum mechanics gave scientists insight into how things work at a sub-atomic level and postulates a theory that might be true for both transactions and sub-atomic physics—an observer must necessarily affect the outcome of what he observes. Therefore, it might sometimes be best in physics and transactions just not to look.

This is what isolation means—Transaction A might not see or influence the data as it is being manipulated by another transaction, Transaction B. Transaction A might see the data before Transaction B operates on it, or after Transaction B commits, or rolls back, but cannot see or influence anything in between. If you are a fan of quantum mechanics, think of a transaction as unwatched particles expanded into their wave probabilities, exploring every possible trajectory at once.

Durability

After changes are committed, the changes must be durable. If the power is switched off, the changes must still be there when the power is turned on. In other words, when a change is committed, the transaction is over and the modifications are permanent.

A Transaction Scenario

Now that you're familiar with the concept of what transactions are, let's get into some examples. In a relational database system, all modifications occur as a result of an INSERT, DELETE, or UPDATE statement. If you are not familiar with relational databases, you can still follow these examples—just keep your eye on the INSERT.

Imagine you have an accounting database where different ledgers or accounts are represented by different database tables. The golden rule of accounting is that everything should balance; so for every debit, there must be a corresponding credit, and vice versa. For example, if you want to pay a $100 bill to XYZ Corp, you debit Cash for 100 and credit Accounts Payable for 100. Imagine that these two T ledgers (so called because they often resemble a capital T when drawn in paper ledger books) are represented by database tables. The SQL that the accounting software sends to the database to make these changes to these tables might resemble the following:

Insert into cash
(
explanation,
debit
)
VALUES
(
'payment to XYZ corp. for widget',
100
)

Insert into accounts_payable
(
explanation,
credit
)
VALUES
(
'payment to XYZ corp. for widget',
100
)

The preceding SQL works fine most the time, but in computer science it is always the boundary, fringe conditions that take the most work to accommodate. What if the system crashes just after executing the first statement, debit from cash, but before applying the cash to Accounts Payable? When the system comes up again, your books will be out of balance. Money has been taken from cash, but not applied to any other account. You lost money, got nothing for it, and this glitch will likely be buried by thousands of subsequent entries applied after the system comes up again, making it very difficult to track down. How can you prevent this from happening?

The BEGIN TRANSACTION and COMMIT TRANSACTION Commands

If you make both of these statements an atomic unit of work by wrapping them in a transaction (atomicity), you are assured that data will always be consistent (consistency) because all transactions will be isolated from one another until their unified success or failure (isolation) and that their ultimate modifications will be a permanent, durable part of the database (durability). In other words, rewrite slightly to add the BEGIN TRANSACTION and COMMIT TRANSACTION statements to produce the following:

BEGIN TRANSACTION

Insert into cash
(
explanation,
debit
)
VALUES
(
'payment to XYZ corp. for widget',
100
)

Insert into accounts_payable
(
explanation,
credit
)
VALUES
(
'payment to XYZ corp. for widget',
100
)

COMMIT TRANSACTION

Distributed Transactions

Relational databases typically implement transactions (and other features, such as replication) by keeping log files. Log files record all data modifications made to the database. In the simplest implementation, when a DBMS encounters BEGIN TRANSACTION, it writes all data modifications from that point on to the log. In this way, if it encounters a ROLLBACK command, it systematically works backward through the log and undoes any changes that have been made up until the BEGIN TRANSACTION is reached. If a system crash occurs, the DBMS notices upon restart that the transaction did not complete and likewise undoes all changes. If, on the other hand, the database encounters a COMMIT statement, the changes are made permanent, the log may be purged (or truncated), and the transaction is closed out.

This is a simplified case. Logging strategies differ widely among database systems—some keep a perpetual log for replication and transactional purposes, others truncate their log files after a transaction is complete. Whatever they do, however, is implementation (and setting) dependent and need not concern you. It is the transaction that is important.

So, transactions are relatively straightforward when you have one database. Any user can, at any time, group a series of data-modification statements between a BEGIN TRANSACTION and COMMIT and rest assured that all SQL statements will either all succeed or all fail atomically. Suppose, however, that a developer wants to have a transaction that spans multiple database systems, perhaps from different database vendors? Suddenly things become more complex.

A transaction is normally specific to a particular database connection. (The system interprets a broken connection or dead client as a reason to ROLLBACK.) The developer needs a different database connection for each database he wants the transaction to span, but how can he juggle the transaction between these databases over different, isolated connections? Internally, databases from different vendors use entirely different, incompatible mechanisms for handling transactions.

Clearly some form of third-party referee or transaction monitor (TM) is needed to coordinate transactions across multiple databases, and all databases must support some form of industry-standard protocol to communicate with this TM. We discuss the database-to-TM protocols in more detail in the Chapter 9, "Compensating Resource Managers." For now, let's take a look at Microsoft's transaction manager, the Distributed Transaction Coordinator (DTC).

The DTC

If you have ever bought any form of real estate in the United States, you know exactly how the DTC operates. To protect two or more parties (usually a buyer and a seller) who want to enter into a real-estate or other business transaction, a protocol has evolved to ensure that all parties participating in the transactions successfully discharge their contractual responsibilities. If all parties do, the transaction completes; if one or more do not, the transaction is aborted.

Escrow and the Two-Phase Commit

This protocol centers around something called escrow. I am simplifying somewhat, but escrow can be thought of as a neutral third party acting as a temporary holder of assets and a kind of transaction referee. The way it works is—if you agree to buy my house, we have effectively agreed to enter into a transaction together. For you to buy my house, you need to give me money, and I need to give you the title. Sounds simple, but a lot can happen in between.

If you give me the money first, but I don't give you the title after depositing your check, the transaction is not complete. However, there isn't much chance that I'm going to return the money, and you will have a very difficult time finding me, I can assure you. So, I am a fugitive of the law, and you are out a lot of money with no house to show for it. There is no way to roll back. On the other hand, if I give you the title to my house first, you might move in, change the locks, and never give me my check. I have no legal recourse because, officially, you own the house. Again, we have an aborted transaction leaving one of the transaction's participants out of balance.

The escrow system was developed so that a neutral third party can mediate the transaction and prevent these kinds of problems. In the escrow way of doing things, the buyer and seller agree on a third-party agent, an escrow agent, who has the following responsibilities:

  • Holds all assets in an escrow account, including house title and cash. Neither the buyer nor seller has access to this account.

  • Makes certain that both parties do what they promise before releasing any assets and closing out the transaction.

  • Returns assets to original owners if either party fails to perform his contractual duties, thus aborting the transaction.

  • Acts as the guardian of the transaction.

If we use an escrow agent, let's call him David T. Cohen (DTC for short), our transaction is not much more complex but is safer for both parties. You and I agree to engage DTC or, in database terminology, you and I enlist in the transaction with DTC.

Each of us is hoping for a permanent change of our fortunes—you want the title, and I want the money—but we need to let the DTC manage this process. Normally, in real estate, the buyer and seller do not have any direct contact with one another; it is all done through an escrow attorney.

The Two-Phased Commit Protocol

At this point, the following two phases must occur before DTC is happy and allows the house to change title from me to you:

  • Phase One (Prepare). DTC asks each of us to prepare to discharge our specific financial responsibilities. This preparation is unique to each of us. You should probably get a certified check from your bank for the purchase price of the house, and I need to dig up the deed for my home. If your bank says you don't have the funds, or if I can't find my deed in the attic or my kitchen junk drawer, we need to tell DTC. The DTC then tells the other party that the transaction is over. If we both can complete our preparations, we tell the DTC that we are both prepared.

The DTC Does Not Know Details

In real life, your money and my deed are placed in an attorney's escrow account after the Prepare phase, but a database DTC does not have any knowledge of or interaction with actual resources and data being modified.

  • We have completed the Prepare phase, but the change of title or acceptance of money has not occurred yet. No official change of record takes place for either of us. However, because we have each indicated that we are prepared, we are obligated to go through with this transaction if and when the DTC asks us to sometime in the immediate future.

  • Phase Two (Commit). After we have both indicated that we are successfully prepared, the DTC notifies both of us that the sale will go through. We need to sign the final contracts and make whatever changes necessary to make the deal permanent. At this point, you can put the deed in your safe, and I can put the money in the bank. There is no going back at this point. Our states are permanently changed.

During the Prepare and Commit phases, in broker's lingo, the house is said to be under contract. This means no one else can bid on it or deliver another offer to the seller. What's more, specific information about the transaction is not made available to parties outside of the transaction until it is concluded. This provides the same effect isolation does according to ACID.

System Failure and Reconciliation

The DTC is a busy guy, and he only waits around for a set period of time when listening for a response from the buyer or a seller. During thePrepare phase, if the buyer or seller does not contact the DTC for some set period of time, the DTC calls off the whole transaction, and he notifies all parties that the deal is off.

If both parties indicate that they are prepared, the DTC then asks both parties to commit. If he doesn't hear from someone for some set period of time thereafter (maybe the seller slipped in the shower), the DTC becomes uncertain about the entire transaction. There is still the possibility of a reconciliation, however, after the seller regains consciousness and checks in with the DTC.

If both parties successfully complete the Prepare andCommit phases, the DTC's job is done.

Microsoft's DTC: The Reality

If you understand the preceding analogy, you understand the basic function of the real DTC, Microsoft's Distributed Transaction Coordinator. The DTC acts as a managing third party that a client application can call on to enlist two or more databases in one distributed transaction. The DTC that the client contacts becomes the controlling DTC and initiates contact with DTCs on each machine where a participating database is located. Together, they work under the management of the controlling DTC to make sure that all participating databases succeed in their data modifications, or none of the modifications are allowed to take place. In this way, the DTC facilitates a distributed transaction that can span many databases.

The DTC does so by employing the same two-phased commit discussed in the preceding section. Phase one involves asking the participants, each in turn, toprepare to make their proposed modifications. The second phase involves actually asking them tocommit these changes. Any discontinuity or failure occurring with any participant in either of these two phases results in the DTC demanding that each participant roll back. In this way the entire conceptual transaction can be rolled back. We revisit the two-phase commit in greater detail in Chapter 9.

My escrow analogy is okay for explaining the concept of the DTC, but it is imperfect in one regard. An escrow attorney actually has knowledge of the details of the transaction. He knows that a house is being sold, and he even holds onto the deed and payment temporarily. A real DTC, however, knows nothing about what the participants are trying to accomplish. It relies entirely on the participating databases to inform it of their success or failure (or failure to report at all, which is seen as failure) and simply uses this knowledge as it walks them through the two-phase commit, telling the participants what they should do.

Microsoft's DTC first shipped with SQL Server 6.5 in April 1996. Specifically, it runs as an NT service, and it is always listening for some database client to call out and request its service. And call out they do. Let me show you how.

Using Raw DTC to Coordinate Transactions Across Multiple Databases

Like all COM+ services, the mystery surrounding transactions quickly dissipates if you understand the underlying internal processes. In this spirit, then, I propose the following scenario: Process A wants to make a data modification on Database 1 (DB1) and Database 2 (DB2). These two database modifications are, however, related, and must succeed or fail together. A distributed transaction is needed to span the two databases.

It is the responsibility of the client application to request the assistance of the DTC and enlist all the database connections it holds in the distributed transaction. The exact mechanism differs depending on what database API the client is using. Generally, there are three steps:

  1. Contact a DTC on a specific machine (or default to the local) and ask for a new ITransaction interface.

  2. Enlist each database connection in the transaction with this interface.

  3. Commit the distributed transaction by calling ITransaction->Commit().

Steps 1 and 3 seldom vary, but step 2 differs, again, depending on what client-side database access technology you are using. If Open Database Connectivity (ODBC) is being used, Listing 8.1 demonstrates how to use the DTC to create a distributed transaction.

Example 8.1.  Using the DTC to Create a Distributed Transaction

//This is a C++ client application that has two database connections
//on two different database servers on different machines.

//It demonstrates how to use the DTC to create a distributed transaction
//that spans both databases.

//Also note that there will actually be two DTCs involved. The DTC
//we are contacting here will be the controlling DTC and it will
//automatically coordinate with the DTC on the second machine
//behind the scenes.

ITransactionDispenser *pTransactionDispenser;
ITransaction          *pTransaction;
HRESULT hr;

//below: perform ODBC style login
//and put valid connection handles
//into gCon1 and gCon2
MyDBLogon(&gCon1, "server1", "sa", "sasparilla");
MyDBLogon(&gCon2, "server2", "sa", "humtulumpus");
//Step 1
hr = DtcGetTransactionManager(
                       NULL, //get the DTC on the local machine
                       NULL,
                       IID_ITransactionDispenser,
                       0,
                       0,
                       NULL,
                       (void **)&pTransactionDispenser
                       );

hr = pTransactionDispenser->BeginTransaction(
                       NULL,
                       ISOLATIONLEVEL_ISOLATED,
                       ISOFLAG_RETAIN_DONTCARE,
                       NULL,
                       &pTransaction
                       ) ;

//Step 2 in ODBC:
SQLSetConnectOption (gCon1->hdbc,
                       SQL_COPT_SS_ENLIST_IN_DTC,
                       (UDWORD)pTransaction);

SQLSetConnectOption (gCon2->hdbc,
                       SQL_COPT_SS_ENLIST_IN_DTC,
                       (UDWORD)pTransaction);

//execute on database 1
MyExecuteStatement(&gCon1,"delete from current_customer where custid=22");
//execute on database 2
MyExecuteStatement(&gCon2,"delete from legacy_customer where custid=22");

// Step 3: Commit the distributed transaction
hr = pTransaction->Commit(0,0,0);

pTransaction->Release();
pTransactionDispenser->Release();

//deallocate ODBC handles

The following are the basic steps outlined in the code:

  1. Call DtcGetTransactionManager() to get a transaction dispenser. (Note: This function does not return a true COM interface, however, just a vtable. This is why you don't see any calls to CoInitialize() and the like—it looks like a COM, but COM is not involved.)

  2. Call ITransactionDispenser->BeginTransaction to obtain an ITransaction pointer.

  3. Enlist your separate database connections with the ITransaction pointer.

  4. Execute your SQL statements.

  5. Call ITransaction's->Commit() to commit the transaction.

This general flow is always the same for client applications using the DTC directly. Obviously, the client applications need to be able to use COM, but their interaction with the DTC is pretty straightforward—request a couple of transaction-oriented interfaces and call their methods. Only the enlisting of the transaction differs significantly, depending on what database technology your client uses.

Differences in Transaction Enlistment

If your client is using ODBC, its database connections are enlisted in transactions via the SQLSetConnectOption() function:

SQLSetConnectOption (gCon2->hdbc,
                     SQL_COPT_SS_ENLIST_IN_DTC,
                    (UDWORD)pTransaction);

If your client application is not using ODBC, the enlistment process is different. For example, if you are only using Microsoft SQL Server databases and choose to use SQL Server's native database library,dblib, you enlist your transactions this way:

dbenlisttrans (dbp, pTransaction);

There are, of course, differences between dblib and ODBC in terms of how database connections are made and SQL statements are executed. There is one important thing to note while we are on the subject of dblib—unlike ODBC, dblib does not support the automatic transaction enlistment that gives rise to COM+ transactions. dblib cannot participate in implicit COM+ transactions because it does not support the interfaces necessary to coordinate with the COM+ Dispenser Manager.

We talk more about the Dispenser Manager in the upcoming section, "Resource Dispensers: A First Look." For now, you need only understand that although COM+ objects can use dblib to connect with SQL Server, they do not get automatic, context-based transaction support. It is best to stick with ODBC.

Transaction Enlistment by Pooled Objects

Transaction enlistment is automatic except in the case of pooled objects that participate in transactions. Object Pooling is discussed in Appendix C, "Object Pooling," but the premise is simple and can be described here. When an object is released by the client, if that component is configured to be pooled, COM+ will not destroy it. Instead COM+ keeps it alive but deactivated in some form of object pool. Then, when an object of this type is requested at a later time by another client, COM+ will reactivate a pooled object instead of creating a new instance. The client will get an interface to this pre-existing object and be none the wiser. Obviously, pooling improves performance.

Unfortunately, pooling does complicate transaction enlistment. Resource Dispensers (RDs) automatically enlist connections on behalf of the objects at the time the objects first receive the connections. Pooled objects, however, hold on to the same database connection no matter how many different parent objects they may ultimately serve because obtaining a database connection is an expensive process. Because different parent objects may be participating in different transactions, it is necessary for a pooled object upon reactivation to determine what transaction its new parent is participating in. Because context will flow from the parent to the pooled object, the pooled object need only investigate its own context to find the transaction identifier it has inherited from its new parent. If the object determines that the transaction has changed (that is, it has a new parent in a different transaction) it must obtain an ITransaction pointer from its context and manually enlist its database connection with this new transaction. In the "IObjectContextInfo" section of Chapter 7, "Contexts," I include a code example that demonstrates how a transactional, pooled object determines if its transaction had changed. In the Chapter 7 example, if the object finds that its transaction changed, it calls the EnlistDTC() method. We are now ready to see what is inside this method. Listing 8.2 shows the contents of EnlistDTC().

Example 8.2. A Pooled Object Reenlisting a Database Connection with a New Transaction

// This method enlists the current transaction with the DTC.  COM+
// usually does this implicitly when an object is created, but in the
// case of a pooled object that changes transactions without ever being
// destroyed, re-enlistment must occur.  This involves:
//
// 1. Obtaining an ITransaction pointer from IObjectContextInfo::GetTransaction
// 2. Using the ITransaction pointer to enlist in the transaction with the DTC.
//
// This code was adapted from the account.vc example, included in the Platform SDK.

HRESULT PooledObject::EnlistDTC() {

// Error checking omitted for brevity:

HRESULT hr;
IObjectContext * pIObjectContext;
IObjectContextInfo *pIObjTx;

// Obtain IObjectContext and IObjectContextInfo interfaces:

hr = CoGetObjectContext(IID_IObjectContext, (void**)&pIObjectContext);

if (hr!=S_OK) {
    return hr;
}
hr = pIObjectContext->QueryInterface(IID_IObjectContextInfo, (void **)&pIObjTx);


if (SUCCEEDED(hr))
{
    // Retrieve the ITransaction pointer from IObjectContextInfo
    ITransaction *pITx;
    RETCODE rc;

    hr = pIObjTx->GetTransaction ((IUnknown **)&pTx);

    if (SUCCEEDED(hr)) {

        // Enlist with the DTC.  This is a demonstration of
        // enlistment using ODBC - it will differ depending
        // on the Resource Manager the component is using.
        // In this case, we are using
        // an OLE-DB provider on top of ODBC.

        rc = SQLSetConnectOption(m_hdbc, SQL_ATTR_ENLIST_IN_DTC, (UDWORD)pTx);

        if ((rc) != SQL_SUCCESS && (rc) != SQL_SUCCESS_WITH_INFO) {

            hr = E_FAIL;
        }

        pITx -> Release();
    }

    pIObjTx -> Release();
}

pIObjectContext->Release();
return hr;

}

VB6 objects cannot, as yet, be pooled. So, although they cannot benefit from the increased performance of pooling, they do not need to be concerned with manually enlisting their connections in transactions.

Summary of Distributed Transactions and the DTC

If you have never worked with distributed transactions before, you might be surprised at the relative simplicity of the preceding code. All the real difficulty involved in coordinating distributed transactions is handled by the DTC and the database systems themselves. For an in-depth look at the DTC, consult the book's sample code (http://www.newriders.com/complus) on Resource Managers. Among other things, this code demonstrates how DTCs on different machines communicate with one another.

Regardless of how the DTC performs its magic on the lowest level, at the highest level all the developer needs to do is inform the DTC of his intentions using a couple of simple interfaces (ITransaction, ITransactionDispenser). Then, with only a couple of additional Win32 API calls (DtcGetTransactionManager, SQLSetConnectOption, or dbenlisttrans), the functionality comes for free.

Keep in mind that all the distributed transaction functionality described here has been around since SQL Server 6.5 shipped, and you can see that COM+ does not add much. COM+ simply leverages the capabilities of the DTC and the transactional capabilities of most relational databases. COM+ does provide, however, a simplified, more abstracted method of using this capability without requiring the object developer to be familiar with the DTC. COM+ objects that participate in transactions do not need to call any Win32 API functions, nor do they ever need to request and manipulate any DTC-specific interfaces in the manner demonstrated in Listing 8.1.

COM+ Transactions

Science fiction writerArthur C. Clarke once wrote, "Any sufficiently advanced technology is indistinguishable from magic." Of course, advanced is a relative concept. If you don't understand the role of the DTC and are not aware of how the data modifications of COM+ objects are automatically enlisted in transactions by COM+, it all seems like magic. If you do understand these processes, however, the magic and mystery dissolve, and COM+ becomes entirely fathomable. Toward this end, it is now time to take a look at a simple COM+ transaction and discuss what goes on under it.

COM+ Transaction Declarative Settings

Let's assume that you have two configured COM+ objects, A and B, both of which are going to modify data on two different databases. A creates B. First, how does COM+ know that these two objects are transactional? Second, how does COM+ know that the two objects are related and will be working together under the umbrella of one distributed transaction?

The answer to the second question is COM+ does not know that these objects will be working together. And the answer to the first question explains why COM+ doesn't need to know—both objects are configured in the COM+ catalog as one of the following, shown in Figure 8.1:

  • Disabled(Developers only). Seldom used except in rare situations where the developer wants to work with the DTC directly.

  • Not Supported(Don't participate at all). If A has a transaction and creates B, B wants nothing to do with it. What B does with a database (if anything) has nothing to do with A. B lives in a world (context) where there is no such thing as a transaction, and it doesn't want to deal with one in any way. In a sense, the transaction stops dead at B's doorstep. B does not propagate the transaction downstream to any objects it might create.

    componentstransaction propertiespropertiestransaction, componentsTransaction properties for a component.

    Figure 8.1. Transaction properties for a component.

  • Supported (Participate but don't originate). B should know about transactions and can participate in them if it is created by an object that has a transaction in its context. So, if A has a transaction, and it creates B in the context of that transaction, then B's data modifications should succeed or fail in concert with A's. What's more, B propagates the transaction to any objects it creates if the context of those downstream objects doesn't forbid it. B can call its object context's IsInTransaction() method to determine if it is participating in a transaction or not.

  • Required (Use existing transaction if available; create a new one if not). If A's context has a transaction, B inherits it, participates in the transaction, and passes it on to objects it creates. If A does not have a transaction, B creates a new transaction and, in the process, becomes the root of that new transaction. The term Required might lead you to believe that an object so marked demands that a transaction be present in the context of its creator, A, and that COM+ will not allow B to be created if B's creator lacks a transaction. This last assumption is not true, however. An object marked Required is saying that it needs a transaction one way or another, and if its creator doesn't supply one, B creates a new one and passes it downstream to objects it creates.

  • Requires New (Demand a new transaction). B needs its own transaction. In B's context, only its own transaction is important; it is only interested if objects that it creates succeed or fail in their data modifications. It does not care what transaction came before it, because an object so configured will always create a new transaction of its own. This setting is often used when you want to nest one transaction in another. For example, if you want to keep some permanent record, perhaps a transactional event log of each attempt at data modification by the outer transaction, Requires New is a good setting for the inner objects that have the job of logging the attempt. This way, the inner logging objects and the outer data modification objects can commit or abort independently. Remember, if both the data modification objects and the logging objects share the same transaction, a SetAbort() in any object ultimately causes the data modifications for all the objects to be rolled back. The result is no data modification, but also no log of the attempt.

By setting the configuration options for objects A and B, COM+ knows how the objects are to behave in all transactional scenarios. Again, you are confronted with COM+'s declarative model. A functional mindset writes A and B so that they work with a priori knowledge of one another and programmatically enlist their transactions with the DTC. This is in sharp contrast to the COM+ declarative mindset that dictates that you state how the objects will behave in any situation.

We trust that COM+ can handle any discontinuity between objects whose configured attributes conflict by using interceptors or by disallowing the cooperation altogether.

In this case, assume A is configured as Requires New and B is configured as Supported. These settings allow the transaction to flow from A to B. The stage for the distributed transaction is now set. The actual source code for A and B is in Listing 8.3 and 8.4.

Example 8.3.  Component A Source, Crediting an Account Database

Sub CreditAccount()

'A reference to the component that will Debit Accounts:
Dim DebitComponent As ComponentB

'An ADO Connection object to perform database manipulation:
Dim adoConn As New ADODB.Connection

'If we encounter any problems abort the transaction:
On Error GoTo Problem

'Obtain an instance of ComponentB, and tell it to perform its
'operation (debit the account):
Set DebitComponent = New ComponentB
DebitComponent.DebitAccounts

'Assuming the operation above proceeded smoothly, credit the
'appropriate account:

'Establish a connection with SQL Server:
adoConn.Open "Driver={ SQL Server} ;Server=Tetley;Database=CreditAccount"

'Add 10% to the account balances in the CreditAccount Database:
adoConn.Execute "UPDATE CreditAccountTable SET AccountBalance =" & _
                (AccountBalance * 1.1)", Connection, _
                 adOpenDynamic, adLockOptimistic
adoConn.Close

'Let COM+ know everything went smoothly by calling SetComplete:
GetObjectContext.SetComplete
Exit Sub

Problem:
'There was a problem, so tell COM+ to abort the transaction
'by calling SetAbort:
GetObjectContext.SetAbort

End Sub

Example 8.4.  Component B Source, Debiting an Account Database

Sub DebitAccounts()

'ADO Data objects we will be using:
Dim adoConn As New ADODB.Connection

'If we encounter any problems, we will abort the transaction:
On Error GoTo Problem

'Establish a connection with SQL Server:
adoConn.Open "Driver={ SQL Server} ;Server=Tetley;Database=DebitAccount"

'Subtract 10% to the account balances in the DebitAccount Database:
adoConn.Execute "UPDATE DebitAccountTable SET AccountBalance =" & _
                 "(AccountBalance / 1.1)", _
                  Connection, adOpenDynamic, adLockOptimistic

adoConn.Close

'Let COM+ know everything went smoothly:
GetObjectContext.SetComplete
Exit Sub

Problem:

'There was a problem, so tell COM+ to abort the transaction
'by calling SetAbort:
GetObjectContext.SetAbort

End Sub

As you can see, we are using ActiveX Data Objects(ADO) for database manipulation. ADO is the preferred database access technology for a number of reasons outlined in Appendix A, "ADO and OLE-DB," but RDO or the direct ODBC API also can be used.

Notice that neither A nor B do anything other than san update statement to the database for which they have a connection. They do not explicitly engage the DTC, and they don't share any interfaces or other data. Both A and B do call SetComplete() (or SetAbort() if they encounter an error), but other than that, A simply creates B in an ordinary COM fashion.

Where to Find the Source Code

Visual C++ and Visual Basic source code can be found on this book's Web site: http://www.newriders.com/complus.

If A completes its data modifications and calls SetComplete() but B calls SetAbort() (or has an access violation, hangs indefinitely, or so on), all the changes for A are ultimately rolled back. The reverse is also true—if B succeeds but A fails, the data modifications for A and B are undone as the distributed transaction is aborted.

It is very important to realize, however, that SetAbort() and SetComplete() do not initiate any kind of immediate ROLLBACK or COMMIT action at the time they are called. It is tempting to think of SetAbort() as triggering an immediate RDBMS ROLLBACK statement and SetComplete() as firing a COMMIT statement, but as you see in the upcoming section, "COM+ Transaction Behavior: Voting," this is not the literal truth. That said, conceptually at least, this line of reasoning is not far off—a SetAbort() call probably does result in a ROLLBACK (and SetCommit() results in a COMMIT) statement to be issued down the line, but it doesn't happen at the moment SetAbort() or SetComplete() are called. In the section "COM+ Transaction Behavior: Voting," the reasons for this delayed committing are made clear.

Returning now to the code in Listing 8.3, clearly A and B are sharing in a distributed transaction mediated by the DTC, but how is COM+ making this happen? This question can be answered in multiple parts. This first part has to do with Resource Dispenser(RD).

RDs: A First Look

The concept of a RD is simple—a Dynamic Link Library (DLL) that creates and hands out resources such as database connections, threads, or live connections to any resource. RDs are, in themselves, architecturally interesting. In the book's Resource Manager (RM) source code, you find an example of how they operate and interact with other components of COM+.

As I've said, an RD is a DLL that hands out resources. Database connections are resources, and in fact, the ODBC driver manager is a COM+-compatible RD. This means that no matter what database access technology A and B use (ADO, RDO, ODBC API) to get an ODBC connection, they ultimately come into contact with the ODBC driver manager/RD. However, to support COM+ transactions, an RD must do more than simply hand out database connections to requesting objects. Specifically, the RD must be able to look at the context of the object to determine if that object is in a transaction. If the object is in a COM+ transaction, the RD must enlist the connection it gives to the object with the DTC.

The ODBC driver manager is an RD, but RDs cannot investigate the context of objects requesting resources from them on their own. There is another COM+ entity called the Dispenser Manager (DispMan) that has precisely this job. When an RD is first called upon by a client, the RD registers itself with DispMan (all RDs must if they want to propagate transactions). After DispMan and the RD have established a relationship (they hold interfaces to one another), they work together—the RD tells DispMan that a client object has requested a connection, and DispMan investigates the context of the requesting object on behalf of the RD and tells the RD whether to enlist the connection in a distributed transaction with the DTC.

The relationship between DispMan, the ODBC driver manager, and other RDs is interesting, complex, and worthy of detailed discussion. In fact, the Resource Manager example included with the book's source code is dedicated to exploring the roles and interrelationships of RDs, RMs, and DispMan. For now, realize that DispMan and the driver manager work together to determine whether there is a transaction in the object's context and, if so, enlists the database connection with the DTC. This is how COM+ transactions occur.

COM+ Transaction Behavior: Voting

Now that you understand the basic mechanisms underlying COM+ transactions, you can learn the behavior of objects that participate in them.

I have said that objects can vote on the outcome of a transaction by calling SetAbort() or SetComplete() on the IObjectContext interface (introduced in Chapter 7) they obtain from COM+. The question then arises, when are the results of this vote tallied?

Contrary to intuition, a distributed transaction is not necessarily rolled back the moment an object calls SetAbort(). In this way, SetAbort() is not like ROLLBACK TRANSACTION which has an immediate effect on a relational database. For an object, SetAbort() initiates no action whatsoever—all it does is flip the object's commit bit (this bit is in the object's context) to 0. This bit only becomes significant when COM+ systematically checks the status of all participating objects. This occurs when the root object (the object that originated the transaction) has completed its work and is deactivated.

COM+ attempts to commit a transaction when the root object that originated the transaction deactivates. The root object might deactivate when the object has called SetAbort() or SetComplete() (or even SetDeactivateOnReturn(TRUE)) and has returned from a method call to its client. As you see in the next section and all subsequent sections of this chapter, calling these methods flips one or both status bits in the object's context. Although these status bits can let COM+ know that the root object is allowed to be deactivated, they don't directly cause the object to deactivate. Another entity known as JITA (Just-In-Time Activation) does this.

Deactivate

The phrase completed work is somewhat vague, and the term deactivate can further furrow the brow. It is important to realize that, in COM+, objects are not necessarily deleted when the client releases all references or completes a method call to them. If the object supports pooling, it is disassociated with the client and put in a pool to be used again by other clients. If the object does not support pooling, it is destroyed. The term deactivation covers both cases. Because VB objects do not support pooling, they are destroyed when deactivated.

In the preceding case, COM+ attempts to commit a transaction when the root object has completed its work and is deactivated. Somewhere up the creation chain, however, there must be some client EXE that created the root object. This brings about a third scenario where COM+ can try to commit a transaction: The client EXE itself calls SetAbort() or SetComplete().

Base Clients, Voting, and the TransactionContext Object

It might seem impossible for a base client (another term for aclient executable) to vote on a transaction given that SetAbort() and SetComplete() are methods of the context object. Remember, a client executable does not run in COM+, so it does not have a context. Without a context, any request the client executable makes to obtain an IObjectContext or an IContextState interface fails. You might, therefore, imagine that it is impossible for a client executable to call SetAbort() and SetComplete() directly. You would be right, except that COM+ provides a special object that a client executable can create and then use to vote on a transaction. The following code, shown in both VC++ and VB, creates a transaction object that a client executable (lacking a context of its own) can use to vote on the transaction it kicked off by creating first (root) transactional object:

In VC++:

ITransactionContextEx *m_pTransactionContext
CoCreateInstance(CLSID_TransactionContextEx, NULL, CLSCTX_INPROC,
                 IID_ITransactionContextEx,
                (void**)&m_pTransactionContext);

In VB:

Dim myTranContext As New TransactionContextEx

The only hitch is the client EXE must use ITransactionContextEx->CreateInstance() to create the first object. This might seem very similar to MTS coding where it was necessary for object A to create objects using IObjectContext->CreateInstance() so that the context would flow from A to B. The situation you have with ITransactionContextEx is exactly the reverse—COM+ is providing a way for the context to flow from Object A back to the client executable that created it. Technically, the client still does not have a true context, but it does have one of the key advantages of one—the capability to vote on the outcome of a transaction.

The Methods of IObjectContext

Now that we have laid the groundwork for understanding COM+ transactions, let's take a look at each method of IObjectContext. Note that I am not listing these methods in vtable order; rather, they are grouped by category.

CreateInstance: A Legacy Method

The CreateInstance method is included for backward compatibility but need never be used again. In the old days of MTS years prior to COM+, it was important for one transactional object to create another using this method call so that MTS had an opportunity to pass transactional information from the creating object to created object. In other words, this was how context flowed in MTS. Prior to COM+, however, COM had no sense of context, so creating an object with the CoCreateInstance method in C++, or New or CreateObject methods in VB and J++, resulted in an object that did not know about and could not share in its parent's transaction.

Basic Voting and Associated Methods

As we've discussed, transactions are democratic, and every object gets to vote. An object wears its vote much like a politically active person might return from the voting booth wearing a pin on his lapel saying, "I voted for Candidate X."

An object's lapel pin has the form of two bits stored in its context. These two bits are often referred to as the Happy and Done bits (HD). When an object calls SetAbort(), SetComplete(), DisableCommit(), or EnableCommit(), all these functions are doing is flipping these bits into one of four configurations, which follow:

  • I am happy and finished with all my work: HD 11.

  • I am unhappy with what I've done, but am finished: HD 01.

  • I am happy so far, but am not done; things could still go wrong: HD 10.

  • I am unhappy so far, but am not done; things might improve : HD 00.

Let's take a look at how each of the four voting methods map to these configurations:

  • SetComplete(): HD 11

  • An object calls this method to indicate that it has performed all of its database modifications, and they have been successful. This method flips a bit in the object's context (sometimes called the Happy bit) and COM+ interprets that as a yes vote. Remember, SetComplete() does not necessarily initiate any kind of action; you are not calling Commit on your database. It simply flips a bit or a vote that will be tallied later.

  • SetAbort(): HD 01

  • This method is called when an object encounters an error during its data modifications or, for whatever reason, becomes unhappy and wants to abort the transaction (see Listing 8.5). Calling this method registers a vote; it does not initiate an immediate action. Specifically, the Happy bit is flipped to a state of sadness. Note that SetAbort() must be called explicitly by the object to indicate a negative vote; it does not happen automatically in the event of a database error. Thus, it is common to encounter code that checks the success or failure of a database modification method and calls SetAbort() or SetComplete() accordingly.

Example 8.5.  How to Properly Abort a Transaction

Sub DataModify()

'If we encounter any problems, abort the Transaction:
On Error GoTo Problem

<DO SOMETHING WITH DATABASE>

'if an error occurs in VB, control jumps
'to the error block defined in the "On Error" statement.

'if we get to this portion of the code, then
'everything proceeded smoothly.  We indicate this
'to COM+ by calling SetComplete().

'Note that this DOES NOT mean that the transaction will
'commit.  COM+ must tally ALL components particpating in
'the transaction and get their approval before it
'commits.

GetObjectContext.SetComplete
Exit Sub

Problem:

'Let COM+ know there was a problem by calling SetAbort.

GetObjectContext.SetAbort

End Sub

JITA, Object Statefulness, and Stateful Voting Methods

The next two voting methods, EnableCommit() and DisableCommit(), are more complex than SetAbort() and SetComplete(). To understand them fully, we first need to talk about object state.

With the introduction of MTS, the term stateless objects was whispered in the corridors of many an IT shop. Esoteric Socratic discussions of statelessness ensued, and confusion gripped the MTS landscape.

This confusion arose because some said that, to scale well, objects needed some form of pooling and/or automatic deallocation. The traditional client/server mechanism where many clients have dedicated connections to onesingleton server did not scale well. Of course, many developers disagreed. At the time of MTS's introduction, COM allowed (and still does allow) out-of-process or EXE servers. Anout-of-process server gave the developer fine-grain control of threading, memory allocation, object instantiation, and other tools allowing the seasoned developer to write a very scalable COM component if his ability allowed.

The problem with writing a scalable singleton server was that everyone would end up reinventing the wheel. Inevitably, different developers in different places would end up creating schemes for pooling connections, pooling object instances, and so on. This kind of infrastructure was, however, of common use to all object developers, so Microsoft decided to write it for them.

Singleton servers simply did not fit in the MTS model where objects were expected to flit in and out of existence like quantum particles. MTS wanted lifetime control, which meant developers had to give up some control. Specifically, developers had to accommodate the fact that a client application might create an instance of an object, but the client might actually call methods into a different object each time it made a method invocation. After all, you wouldn't want to keep an object alive consuming precious resources for a client that made a method call once a day, right?

But this was a shot across the bow for many developers. If they could not count on the client controlling the lifetime of an object, and could not even assume a dedicated connection, it meant they could not keep state information in member variables or anywhere else in their object. There was, however, something called the shared property manager, which is a kind of global storage area where stateless objects can keep information for their own use or the shared use of other objects. But, responding much like the French mob at King Louis XVI's gates to Marie Antoinette's, "let them eat cake"—riots ensued.

Left out, however, was the fact that an MTS object could keep state. In fact, by default, objects were stateful. The state could only be kept for a limited duration and you would ask for it explicitly only when you needed it. If you treated state as a privilege to be invoked only when needed, as opposed to an absolute entitlement, you opened the way for greater scalability.

This scalability was brought about by something called JITA. JITA (sometimes referred to asJIT) is the consummate downsizer. It tries to deactivate your object whenever possible (usually after a method call returns), and if your object doesn't explicitly state its desire not to be deactivated (either by COM catalog configuration or method call), you cannot be sure when it will be knocked out of commission.

In traditional COM, there was the implicit assumption that when a client application instantiated an object, they were bound to one another via a dedicated connection. Until the client called the final Release() on the object's interface, that particular object instance serviced that particular client. With JITA employed, however, this is no longer true. Your client might call into a different object instance with every method call even though it is holding on to the same proxy. COM+ is keeping the RPC channel alive, but is switching the object instance on the stub side, unbeknownst to the client.

With JITA, it seems impossible to write a transactional object that can keep state. JITA always wants to deactivate your object wherever possible. So the only option it seems is to declaratively disable JITA support for an object in the COM+ catalog. However, all transactional objects must use JITA. Fortunately, getting JITA off your object's back is simple even for transactional objects—just call DisableCommit() or EnableCommit().

  • DisableCommit(): HD 00

    This method flips the Done bit in the object's context. When JITA comes calling and sees this bit flipped, it knows that the object isn't done yet. Although the object might not be doing anything at that moment, it is expecting other method calls from its client. Although JITA likes to deactivate the object as soon as the first method call completes, it leaves it alone and goes off and bothers some other object.

    However, if the root object deactivates or the transaction timeout interval of 60 seconds elapse, JITA comes back with a vengeance. COM+ looks at the combination of the two bits and says, in effect, "I see that you are not done, but it is too late now. You will be deactivated, and I will abort the transaction given that you are obviously not happy."

  • EnableCommit(): HD 10

    The object that calls this method is saying that it is expecting other method calls from its client, is keeping state, and that JITA should leave it alone for the time being. JITA, however, comes back when the root object deactivates or 60 seconds elapse. COM+ then interprets these settings and says, "You aren't done, but I guess whatever else you where planning to do wasn't so important that the transaction should be doomed by your not finishing it. It is time to be deactivated, but because you are happy with everything, I will commit the transaction if everyone else is happy too."

Note that when transactional objects are first activated, it is as if they implicitly call this method. "Happy yes, Done no" is their default disposition. So, if you do not call SetAbort() or SetComplete() before returning from a method call, JITA assumes your object is stateful and does not deactivate it after a method call to it completes.

Basic Informational Methods

The IObjectContext methods that follow in this section are used by objects to gather information about their present context. We begin with a method that allows an object to find out about the security context of the caller:

  • IsCallerInRole. COM+ has a role-based security system (fully described in Chapter 12, "Security") that works with and simplifies NT's more sophisticated authentication paradigm. As its name implies, this method allows the developer to determine if the current user of the object is a member of a particular role group; for example, is he a manager?

    Although security can be set on the interface of the method declaratively, this gives the developer finer-grained control. Declarative security allows you to permit or deny access to a method or interface, but IsCallerInRole might be used when you want to provide access to a method for everyone, but want to change what the method does depending on the role of the user.

  • IsInTransaction. It is as simple as its name implies. Although transaction participation is a declarative attribute, you might want to call this method to make absolutely sure that a given object will or will not function within a transaction in the event the declarative attributes in the COM+ catalog are improperly set. This is probably overkill, but you can imagine writing an object that manipulates incredibly sensitive data, and you want to make certain that it never runs outside a transaction.

  • IsSecurityEnabled. It is possible to set security for a COM+ application so that role-based security is not used. This method returns FALSE in this instance. As with IsInTransaction, this method can be used to enforce security requirements for an object independent of what the COM+ catalog says. Of course, you cannot force role-based security to be reinstated at the moment of a method call, but you can prevent your object from operating without security.

Finer Granularity Control with IContextState

A transactional object can get all it needs from the IObjectContext interface. However, IObjectContext is kind of a blunt instrument when it comes to setting an object's Consistency Happy and Done bits. You might have noticed IObjectContext offers no way to change the consistency or Done bit independently. Each method in this interface sets both bits at the same time.

Another drawback to IObjectContext is that all four of its voting and duration methods assume that you are in a transaction; they all manipulate the consistency bit, as well as the Done bit. Suppose, however, that your object is not in a transaction but wants to flip its Done bit to indicate to JITA that it is ready to be deactivated or wants to stick around. It is true that your non-transactional object still can do this by calling EnableCommit(), DisableCommit(), SetAbort(), or SetComplete(), but these methods result in the manipulation of the consistency bit (which is not looked at in a non-transactional context) and results in misleading source code. IContextState, however, is a refinement of IObjectContext that lets you set the consistency and Done bit separately. It is also logically simpler and can make for clearer code because it only contains methods that have to do with voting on a transaction or deciding whether to allow deactivation. This is in sharp contrast to IObjectContext with its hodgepodge of creation, voting, and informational methods.

Now that I've made the case for IObjectContext, let's look at itsmethods:

  • SetDeactivateOnReturn. Sets the Done bit to 1 or 0 depending on the Boolean that is passed in. It ultimately has the same effect as IObjectContext-> EnableCommit(), but does not affect the consistency bit.

  • GetDeactivateOnReturn. Returns the value of the Done bit as a Boolean.

  • SetMyTransactionVote. Sets the consistency bit to true or false by taking in a type library defined enumeration (see code listing immediately following), and has the same effect as SetAbort() or SetComplete(), but does not modify the Done bit.

typedef enum {
    TxCommit = 0,
    TxAbort = 1
}  tagTransactionVote;
  • GetMyTransactionVote. Returns either TxCommit or TxAbort depending on the value of the consistency bit.

Transactions, ASP Pages, and IIS

In the previous chapter, I speak a little about Internet Information Server (IIS) as Active Server Pages (ASP). For interest's sake, I'm going to revisit this topic because ASP pages have some unique transactional requirements. Read this section only if you are interested in how IIS operates; if you aren't, skip ahead to the next chapter.

Just a few sections ago, I mention that a client EXE needs to create a new instance of TransactionContext object if it wants to vote on the outcome of a transaction. This is because, unlike the objects it creates, an EXE does not have a context and has no access to SetAbort() or SetComplete(). When the EXE creates its first transaction object, it is left out of the loop in terms of voting—the root object it creates might father a whole line of descendents, and the transaction might flow to each one. The client is, however, cut off from the transaction after the root object takes over. Fortunately, the client EXE can borrow a context by creating a TransactionContext object and can use it to both create descendent objects and vote with them.

ASP pages have a similar kind of problem and solution. Much like client executables, ASP pages cannot, on their own, enlist in or vote on a transaction. Although COM objects that the ASP pages create can participate in transactions, suppose you have a series of related ASP pages that do nothing more than modify databases via ADO. How can they use transactions?

The answer is as simple as placing one of the following tags at the beginning of an ASP page:

<%@ Transaction=Required%>

After doing this, the script can, at any point, call ObjectContext.SetAbort or ObjectContext.SetComplete to vote on the outcome of the transaction.

A transaction can span multiple ASP pages providing that a parent ASP page transfers control to another page by using the Server.Execute or Server.Transfer commands:

ASP/VBScript

Server.Execute ("SomeOther.asp") //Server.Transfer would also work

This is eerily similar to the MTS-style of object creation and transaction propagation:

GetObjectContext.CreateInstance("SomeObject.Object")

The principles are quite similar. When an ASP page is run, IIS creates a Page COM object that represents that page. The Transaction= tag at the beginning can make the Page object transactional. And just like in MTS, the transaction can flow to descendent Page objects (other ASP pages) if their Transaction= tags are compatible. Interestingly, the following four components you can find in the IIS Utilities Application are used for this purpose:

  • ASP.ObjectContextTxNotSupported.1

  • ASP.ObjectContextTxRequired.1

  • ASP.ObjectContextTxRequiresNew.1

  • ASP.ObjectContextTxSupported.1

Summary

A transaction is an atomic unit of work that can either be committed permanently or completely undone by a relational database. A distributed transaction is a conceptual transaction that can span more than one database. For distributed transactions to work, an RD, such as the ODBC driver manager, needs to enlist the connections to different databases used by different clients with the DTC. The DTC, by virtue of a two-phased commit protocol, ensures that all participating databases succeed in each of these two phases, Prepare and Commit.

COM+ objects can participate in the same transaction because their contexts allow transaction information to flow from the creator to the created if their transactional settings are compatible. When an object participating in a transaction requests a database connection, the COM+ DispMan checks the context of the requesting object and informs the RD whether it should enlist that connection in the transaction.

If the data modifications made by multiple objects are related, those objects should share in the same COM+ transaction. When participating in the same transaction, any single object can notify COM+ of the success or failure of its data modifications by voting, thereby influencing the success of the overall transaction. An object can vote by requesting an IObjectContext from its context object and calling SetCommit(), SetAbort(), EnableCommit(), or DisableCommit() on this interface. These methods do not immediately cause any action; rather, they flip bits in an object's context called the Happy and Done bits. When the root object of a transaction has completed its work and is deactivated, COM+ inspects these bits in the context of all objects participating in a transaction. By checking the status bits, COM+ can make sure that all objects voted positively. If one or more objects voted negatively, COM+ commands all DTCs involved to roll back their changes.

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

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