by Ted Pattison
Reprinted with permission from Visual Basic Programmer's Journal, 8/98, Volume 8, Issue 9, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com
Information systems based on business objects and an n-tier architecture are becoming increasingly popular. In a two-tier model, client applications connect directly to a DBMS. In an n-tier model, client apps connect to a distributed app that runs a set of business objects in the middle tier. Here you face the challenge of developing a scalable infrastructure with the throughput needed for a large user base. For example, two-tier systems have always been able to leverage the scalability features built into DBMSs such as Oracle or SQL Server for managing connections, pooling threads, and monitoring transactions. But it isn't as clear who's responsible for those functions in an n-tier system.When you're assembling an n-tier app, you'll put a lot of effort into building a sophisticated framework to handle the requirements of a distributed app. Companies that successfully deploy n-tier systems spend an average of 40 percent of their programming efforts on developing this infrastructure. The rest of their effort goes into writing domain-specific code such as business logic and data access code.
Microsoft noted the cost associated with building distributed system infrastructures and realized that you'd rather concentrate on writing business logic. So it took on the task of creating this essential distributed application framework for you.
Of course, Microsoft based this framework on Windows NT and the Component Object Model (COM). Several pieces of this infrastructure are already available, with many more due over the next few years as part of Microsoft's COM+ initiative. One major milestone was the December 1997 release of the NT4 Option Pack, which I see as the first phase of COM+. The NT4 Option Pack provided the initial release of Microsoft Message Queue Server (MSMQ) and improved integration between Microsoft Transaction Server (MTS), Internet Information Server (IIS), and Active Server Pages (ASP). This made it easier for different parts of an app talk to one another. With the Option Pack, you can build apps that leverage the connection pooling, transaction monitoring, message passing, and Web integration built into the NT Server infrastructure.
The key is MTSthe part of Microsoft's distributed framework that helps apps leverage a transaction monitor in the middle tier of a distributed app. But MTS provides more than a transaction monitor. It's also Microsoft's vehicle for shipping the first generation of its distributed application framework. In the future, many of these services will be considered part of the generic COM+ shell rather than part of MTS. However, these changes won't affect the way you create and deploy MTS apps.
MTS will ultimately act as the platform for building online transaction processing (OLTP) systems. The MTS runtime environment runs business objects that access data in the middle tier. The objects use various data sources, such as DBMSs. The MTS transaction monitor makes it easy to define transactions, including ones spread across multiple data sources. MTS does this with a programming model based on declarative transactions, as opposed to programmatic ones. This model can hide proprietary transactioning APIs from MTS programmers, making writing transactions far easier than writing programmatic transactions with ODBC, ADO, or Transact SQL.
Here I'll show you why MTS makes it easier than ever to create distributed OLTP apps. I'll describe how MTS works, explain how you can use it to create apps that run distributed transactions, and provide practice techniques for writing transactional MTS components.
PROVIDE DATA ACCESS WITH MTS
MTS provides a distributed runtime environment for your business objects. You can create MTS components with any COM-enabled language, such as VB, C++, or Java. MTS creates a runtime environment by providing a surrogate process for relating client apps to MTS objects running on NT server (see Figure 1). You deploy MTS objects by loading COM-style DLLs into the MTS environment. VB programmers deploy business objects by creating ActiveX DLLs and installing them in the MTS environment.
You must deploy every MTS component within the context of an MTS package. MTS allows for both server packages and library packages. It activates and runs objects from each server package in a separate instance of the MTS container app mtx.exe (see Figure 1). Objects created from MTS components in a library package are loaded into the address space of the client app that created them. Both types of packages rely on the services of the MTS Executive in mtxex.dll. Here I'll concentrate on deploying MTS objects in a server package.
Figure 2 Instant MTS Components. You can install a VB-created ActiveX DLL into the MTS runtime environment using the MTS Explorer. Each creatable, public class in your DLL becomes an MTS component. |
When you install your DLL in a package, all public creatable classes (also known in COM as coclasses) become MTS components. After that, you can serve up many MTS components from a single DLL. Each MTS component has an associated CLSID, an identifier that any client app wanting to create a new object must use. In this respect, an MTS object works just like any other remote COM object. In COM, clients create and connect to COM objects in a process called activation.
In the world of MTS, you call a remote client a base client. A VB app acting as a base client can activate an object in the MTS environment with either the New operator or the CreateObject() function. When a client tries to activate a new object, it sends an activation request to the Service Control Manager (SCM) on the client machine, which in turn forwards it to the SCM of the computer running the MTS app. The server-side SCM responds by making a local activation request on the package, which is just like any other in distributed COM.
But here MTS gets a little tricky. It looks like a standard COM server to the COM client, but it uses some magic behind the scenes to enhance a typical COM server's functionality. When you install a DLL with the MTS Explorer, the registry settings for the CLSID are altered to change the routing of the activation request. The CLSID is given a [LocalServer32] sub-key with a new path:
\System32\mtx.exe /p:{0EAC413A-4696-_
11D1-B156-0080C72D2182}
The GUID that follows mtx.exe in the command line identifies a specific MTS package. If an instance of the package is already running, the server-side SCM calls into this process, telling it to instantiate a new MTS object from the requested CLSID. If the package lacks a running instance, the SCM launches one and requests the new MTS object. Either way, the SCM and MTS runtime environment work together to create the new object and establish a connection with the base client. As with any other connection involving Distributed COM (DCOM), a proxy/stub layer is introduced between client and object to marshal data back and forth during method calls.
One of the most painful aspects of deploying apps based on DCOM has been client-side registration. The client desktop requires you to specify various registry settings and install a type library. Fortunately, MTS assists with client-side registration by letting you "export" a package. MTS builds a server-side setup script to move an MTS app from a development workstation to a production server. MTS also creates a client-side setup program that registers CLSIDs and installs the required type libraries. In the future, Microsoft will add features to NT5 and COM+ to make client-side registration easier and more foolproof.
MANAGE THE MTS EXECUTIVE
Now you're ready to dig into the MTS Executive (MTX). MTX provides the control center for any app using Microsoft's distributed framework. Think of MTX as a control-freak boss who always has to know what everybody's doing. Any time a client app activates a new object, MTX intercepts the request and oversees object creation. MTX also monitors every incoming method call on every active connection.
Figure 3 Dirty Deeds Done Dirt Cheap. The MTS Executive inserts a context wrapper between a base client and an MTS object. This allows the MTS Executive to monitor all inbound method calls and destroy the object without dropping the client connection.
MTX taps all these method call "phone lines" by inserting a transparent layer called a context wrapper between the stub (that is, the client) and the object (see Figure 3). When a base client creates an object under MTS, MTX intercepts the activation request and connects the client to the context wrapper. MTX then activates the actual MTS object inside the context of this wrapper object, which in turn forwards inbound method calls to the object. The client is oblivious to what's going on. In fact, it can't tell the difference between an MTS object and any other type of COM object. As I'll demonstrate later, the context wrapper even lets MTX destroy the object without dropping the base client connection.MTX also manages the thread pooling required for high-volume distributed apps. In a perfect world, every connected client app would be allocated its own server-side thread. But in the real world, the thread-per-client model dissolves as the thread count goes into the hundreds and beyond. Fortunately, MTX provides an efficient thread-pooling algorithm that balances system responsiveness and scalability.
Microsoft's distributed framework provides a new threading abstraction called an activitya logical thread of execution for one base client. MTS creates a new activity whenever a base client activates an MTS object. This keeps the programming model fairly simple for MTS programmers. Behind the scenes, though, MTX is juggling a pool of 100 single-threaded apartments (COM-aware threads). MTX pools these threads by multiplexing the logical activities in and out of physical single-threaded apartments. Currently those 100 threads are hard-coded into MTS, though I've heard talk about making this number configurable in future releases. At any rate, always set the threading model of ActiveX DLL projects to "apartment" to exploit the MTS threading model.
Bear in mind that each activity belongs to one base client. When a base client activates an object, you call this object the root of the new activity. Once created, the root can create additional objects in the same activity if it makes the activation request properly to MTX. An MTS object cannot properly create other MTS objects by calling the New operator or CreateObject(). For example, if your MTS object calls CreateObject(), the second object will be created in a new activity. This implies that the two server-side objects will run in different single-threaded apartments. Therefore, a proxy/stub layer will be introduced between them, significantly degrading performance.
For a root object to properly create another object inside the current activity, it must call the CreateInstance() method of the ObjectContext interface. The ObjectContext interface lets an MTS object communicate with MTX. To use this interface, include a reference to the MTS type library (MtxAs.dll) in your VB DLL project and call GetObjectContext(). This way, you can create a second object inside the same activity as the root:
' code in root object
Dim ObjCtx As ObjectContext
Set ObjCtx = GetObjectContext()
Dim Object2 As CMyClass2
Set Object2 = ObjCtx.CreateInstance( _
"MyDLL.CMyClass2")
I like to interact with MTX through the ObjectContext interface, which controls numerous services in the distributed framework, such as connection management, threading, security, and the transaction monitor. Someday MTX and the ObjectContext interface will be rolled into the generic COM+ shell and MTS will become another service built on top of it. However, you'll continue to use all these services the way you use them today when building OLTP systems with MTS.
DON'T DROP ACID IF YOU WANT OLTP
Everything I've said so far works for all kinds of distributed apps. For OTLP, though, you need some additional instruction. I'll start with the four essential requirements of a transaction: it must be atomic, consistent, isolated, and durable (ACID). When a transaction is atomic, everything must be written or nothing can be written. Consistent transactions maintain data integrity in the presence of multiple concurrent users. Isolated transactions guarantee that you can't see changes made by other users during transactions until they've been committed. Otherwise, if you could see uncommitted changes made by another's transaction, a rollback would produce inconsistent results. Finally, a durable transaction means that once it has been committed, all the data is written to disk and recoverable.
OLTP systems such as MTS must place locks on various resources to meet the requirements of consistency and isolation. But while you must have locking to guarantee transaction integrity, users must wait for locks to be released. So to optimize concurrency and throughput, you need to minimize locking duration.
MTS provides an infrastructure that makes it easy to obtain and release locks quickly while enforcing ACID's transaction requirements. With MTS, a single transaction can span multiple (even heterogeneous) data sources. MTS uses declarative transactions to take care of the tricky timing and synchronization of the two-phase commit protocol across multiple data sources, simplifying the code you need to write. You just use a handful of methods supplied by the ObjectContext interface to control the flow and cleanup of each transaction.
With MTS, you don't need to write code against proprietary transactioning APIs. Instead, you use a universal transaction API called the Distributed Transaction Coordinator (DTC) and resource managers (see Figure 4). Resource managers are plug-in modules that encapsulate proprietary transactioning APIs for various data sources, including RDBMSs and mainframe apps. Transactional MTS objects read and write to data sources through resource managers. MTX and DTC work together to enlist and monitor resource managers in a transaction. This architecture lets MTS synchronize commit/rollback behavior and the release of all acquired locks across multiple data sources.
Resource managers are currently available for SQL Server, Oracle, DB2, and MS Message Queue Server. I expect to see many more soon. Each new resource manager that comes online will make MTS a more valuable tool for monitoring transactions across heterogeneous data sources. Without a transaction monitor such as MTS, application programmers must hand-code against the proprietary transactioning API of each data source and implement the synchronization code for two-phase commit. This has traditionally proved to be the most costly phase in assembling OLTP systems. MTS tucks all this grungy code behind the scenes.
EXPLORE MTS TRANSACTIONS
To use declarative transactionals, you employ MTS Explorer to set the transactional attribute for each MTS component. Whenever MTX creates a new object, it examines the component's setting to see whether the new object should be associated inside a transaction. Note that after an object is created, you can't change this association. A transactional object spends its entire life inside the transaction it was created in. When the transaction dies, MTS destroys all associated objects along with it. You need this cleanup activity to ensure the proper semantics of a transaction.
Base clients and MTS objects can create new MTS objects. When a new object is created, MTX determines whether the creator is running in an existing transaction and inspects the component's transaction setting. Then MTX knows whether to associate the new object with a transaction. Four transaction settings are possible:
A base client can initiate a transaction by activating an MTS object from a component marked as "Requires a transaction." An object whose creation causes the system to create a new transaction is the root of the transaction and the root of the activity. The methods of the root object can create or "enlist" other objects in the transaction to read and write data to and from their associated data sources. The methods do so by creating objects from components that are marked as either "Requires a transaction" or "Supports transactions." Either way, the new object is associated with the root object's transaction. To be in the same transaction, the objects must be in the same activity. So the root must use the CreateInstance() method to enlist other objects inside a transaction.
Because MTS uses declarative transactions, the code in enlisted objects never has to explicitly start transactions. Once an object is associated with a transaction, it can create a connection using ADO, RDO, or ODBC and start accessing data.
Remember, that control-freak boss-MTX-always knows what's going on. Whenever a transactional object opens a connection through an MTS resource manager, such as the SQL Server ODBC driver, MTX transparently enlists the connection in the transaction and works with DTC to monitor all enlisted connections as a single, logical transaction.
The root object that created the enlisted objects calls on each of them to write changes. If all changes are made successfully, the root object tells MTX to commit the transaction by calling the SetComplete() method in the ObjectContext interface. If any enlisted object in the transaction can't complete its work, it calls SetAbort().
ROOT FOR RESPONSIBILITIES
I've included a sample app for this article, called the VBPJ Market application. It's available on the free, Registered Level of The Development Exchange (see the Code Online box at the end of the article for details). It contains an MTS DLL and a base client app that demonstrate what I've talked about here. Examining this code will help you understand the relationship between root objects and enlisted objects. For example, here's a subset of the code demonstrating the responsibilities of the root and an enlisted object, starting with the appropriate root object method:
Sub SubmitOrder() ' root object: CBroker
On Error GoTo SubmitOrder_Err
Dim ObjCtx As ObjectContext
Set ObjCtx = GetObjectContext()
' create enlisted objects; invoke a few
' methods to write changes it's all:
ObjCtx.SetComplete
Exit Sub
SubmitOrder_Err:
ObjCtx.SetAbort ' if problems arise:
Err.Raise vbObjectError + 1, , "Transaction unsuccessful"
End Sub
You should also look at the methods of an enlisted object:
' enlisted object: CProducts
Function Purchase()
Dim ObjCtx As ObjectContext
Set ObjCtx = GetObjectContext
If(ProductIsInInventory=True)
' ADO code to decrement inventory
CtxObj.SetComplete
Else
' abort entire transaction
CtxObj.SetAbort
Err.Raise vbObjectError + 1, , "Product not in inventory"
End If
End Sub
All paths of execution through these methods result in a call to either SetComplete() or SetAbort(). When an object in a transaction calls one of these methods, it's voting on whether it thinks the transaction should succeed. If an object calls SetAbort(), the transaction can't succeed. If no object calls SetAbort() and the root calls SetComplete(), MTX commits the transaction as soon as the root object's method completes and returns control to the base client. MTS's commit/abort behavior assumes that silence is assent-that if any enlisted objects don't vote, they want their changes committed.
The Purchase() method I've shown demonstrates a common pattern in the method of an enlisted object. If this enlisted object writes its changes successfully, it calls SetComplete(); if not, it calls SetAbort().
The root object will experience a runtime error if it tries to call SetComplete() after another object in the transaction has called SetAbort(). However, MTS doesn't provide a way for the root object to determine whether one of the enlisted objects has called SetAbort(). So an enlisted object must tell the root object when it calls SetAbort(). Do this by raising an error, as I've shown in the Purchase() method. When the root object catches this error, it halts any further work on the transaction, calls SetAbort(), and exits. At this point, I usually throw an error from the root object back to the base client. This lets your MTS app report the failure to the base client and lets you pass a message containing an error description and any possible remedies.
If you don't like to use errors, you can use the return value of each method to return a Boolean true or false indicating success or failure. C-style coders commonly take this approach, but it lacks the elegance of raising errors because your return value indicates the method's status and can't be used for anything else. And if you don't use errors, you must also create an output string parameter in each method to pass an error description from caller to callee. You don't need to do that if you throw user-defined errors.
You only need to call SetComplete() in the root object. You don't have to call it from any of the enlisted objects to commit the transaction. However, I recommend calling either SetComplete() or SetAbort() in any method that obtains locks. This lets you use an MTS component to create root objects or enlisted objects without having to worry about whether you called SetComplete(). It also improves component reusability.
If the root object doesn't call SetComplete() or SetAbort(), the transaction is left pending. If any of the objects in the transaction have acquired locks on resourcessuch as a page of records in a SQL Server databasethese locks will persist after control returns to the base client. They'll remain until the base client initiates another transaction or releases the object. You must limit an MTS transaction's scope to the duration of a single method call from the base client, and either commit or abort the transaction before returning control to the client.
BRAVE NEW PARADIGM
As I've said, for good database system performance, your locks must release as quickly as possible. With MTX, transactions stay alive, locks and all, until you release them by calling SetComplete() or SetAbort() in the root object. When you use these functions, MTS also destroys all the objects associated with the transaction, as part of the cleanup process. So the root object and any enlisted objects live only for the duration of a single method call. OLTP veterans may say, "And your point is?" But this leads to a style of programming new to practitioners of classic OOP and COM.
Although MTS encourages the destruction of transactional objects at the completion of each method, if it puts this responsibility on the base client, you're in for some laborious coding. A base client would have to explicitly create and destroy an object each time it needed to invoke a method. Fortunately, MTX makes things easier with a little hidden trickery: it uses the context wrapper layer to fool the base client.
When a root object calls SetComplete() or SetAbort(), MTX destroys every object associated with the transaction. However, the base client still holds a reference to the context wrapper. When the base client invokes another method call, MTX does a just-in-time activation to put a new root object in place of the first. The client has no idea what's going on. This facilitates OLTP-style programming while cutting down on client-side coding requirements.
OOP and COM programming paradigms fail to address scenarios where state is discarded at the completion of each method call. Object-oriented clients assume they can obtain a persistent reference to a long-lived object. If a client modifies some state within an object, object-oriented programmers assume the object will hold these changes across multiple method calls. Not so with transactional objects in the MTS environment. Every 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 called "stateless programming." And it's not about conserving memory on the computer running MTS objects. It's really about the semantics of a transaction. Once you write a change to a database and release your locks, you can't keep a copy of the data in memory in the middle tier. A copy held by a business object could become inconsistent the instant the locks are released. So statelessness enforces OLTP consistency requirements.
I find I have to remind new MTS users that they're really dealing with two different things: a distributed framework and a transaction monitor.
As a distributed framework, MTS provides a runtime environment for COM objectsthe environment you'll use to deploy distributed business objects in an n-tier architecture.
As a transaction monitor, MTS provides an OLTP app's central nervous system. MTS enforces the ACID rules while transactions are running, and it makes writing transactions as simple as possible. The MTS declarative transactioning model frees you from having to worry about explicitly starting transactions or making calls to proprietary APIs. You just mark components as transactional and call SetComplete() and SetAbort() at the appropriate times. And once you've dragged yourself up this new paradigm's learning curve, you'll never want to use programmatic transactions again.
Ted Pattison is a software developer living in Manhattan Beach, Calif. He offers consulting and mentoring services through his company Subliminal Systems (http://www.sublimnl.com), and conducts intensive classes based on COM and MTS
for DevelopMentor. His book on COM and MTS is due from Microsoft Press in early September. Reach him at TedP@develop.com.