by Jim Karabatsos - GUI Computing
I will probably get a lot of argument about this, but in my opinion the most significant new technologies to come out of the Microsoft stable over the past couple of years are DCOM and MTS. DCOM finally allows objects to be distributed across a network of computers, while MTS provides a common transaction context within which co-operating objects can do their thing and have transaction committal or rollback handled automatically for them by the environment.
Don't misunderstand me -- the technology is not perfect and in some areas its lack of maturity does show through. Having said that, I truly believe that these two technologies represent the next major shift in programming paradigms (to use an overused and often misused word) for the Wintel platform. I think that ignoring these technologies now is extremely foolhardy, even if you are not yet prepared to deploy them.
Microsoft states (and there is ample anecdotal evidence to support this statement) that in a large-scale transaction system, 80% of the work is in the infrastructure. Building a transaction processing architecture that is robust and that scales up to the enterprise is no simple task. MTS is essentially that architecture in a box.
First, we need to come to grips with DCOM because MTS is very much built on top of that technology. Let's take just a few moments to review some concepts.
Before there was DCOM, there was COM (Common Object Model). One of the fundamental precepts of COM is that an object should be useable by a client without the need for the client to know anything about where the object is located and, conversely, the object must be able to react to a client without knowing anything about the location of the client. This concept is called "location transparency". In COM, an object could be developed as an in-process server -- a DLL that runs in the same address space (or virtual machine) as the client -- or as an out-of-process server -- a separate EXE that runs in its own address space. A client could be written and compiled to use a COM object, and the same client would be able to use either kind of server transparently, depending entirely upon which server type was installed and registered on that system.
DCOM (or Distributed COM) is a technology that extends the location transparency of COM across a machine boundary. Just as plain old COM needs to marshal parameters across process boundaries and back to allow cross-process object usage, so DCOM simply enables the same marshalling between a client process running on one machine and a server object running on another. Neither the client nor the server code needs to be aware of this separation. The server is simply written using any tool that can create a COM object (it must, however, be compiled as an EXE or out-of-process server). Similarly, the client just uses the exact same code it used to access any COM object, blissfully unaware that the object is not on the same machine. In its simplest form, DCOM is all handled by the system software and is transparent to all application code.
That's the theory, anyway, and it is true as far as it goes. There are, however, two issues that have been glossed over.
Firstly, the two processes ARE running on different machines. If a client passes, say, a file name of a file on the local hard drive, the server will (generally) not be able to access it. The same would apply to device names, network shares, ODBC connections and possibly user and security information. This is all pretty obvious once you think about it -- I am just making the point that the fact you CAN take a COM server and run it on another machine does not mean that all servers are going to be useable this way.
The second issue is efficiency. Even on a single machine, a cross-process call is an expensive process, both in terms of machine resources used and time elapsed. Typically, a call to an out-of-process server is an order of magnitude slower than a call to an in-process server. With DCOM, it gets even worse -- a call to a remote server is AT LEAST ONE and often TWO orders of magnitude more costly than a call to a local out-of-process server. OUCH. If you take a server that was designed around local access and run it remotely, then you will see some significant degradation in performance.
The trick to making DCOM work effectively is to design the public interface of your server objects so that a single call can achieve a complete operation. In general terms, this means that instead of exposing a series of individual properties that need to be set before a method is invoked, the methods should each take a set of parameters that define the data (or object state) that the method should operate on. That is, instead of designing a server so it is used like this:
MyRemoteServer.SearchField = "Surname" MyRemoteServer.SearchValue = "Smith" MyRemoteServer.Mode = CaseInsensitive MyRemoteServer.Locate CurrentBalance = MyRemoteServer.CurrentBalance
it should be designed for use like this:
MyRemoteServer.GetBalance "Surname", "Smith", CaseInsensitive, CurrentBalance
This achieves two things. The first and most obvious advantage is that the number of network round trips drops from five to one. This is an enormous advantage because in all networks there is a certain overhead per network round trip that is largely independent of the amount of data transferred. Of course the data itself takes some time to move across the network, but our testing has shown that there is no increase in the average elapsed time for a network round trip until the total amount of data transferred exceeds several kilobytes -- the exact number is highly network architecture and load dependent. In the example above, we would expect to see transaction elapsed time drop by about 80%, a HUGE saving when multiplied across many users.
The other advantage is not so obvious and it leads us to MTS. By bunching all required data as parameters to the one method call and returning the required results in the same call (either in a parameter as we have done or as a function result), we are wrapping up all the work into a single object access. We don't care what state the object was in before the call nor do we care what happens to it after the call is complete. In particular, the object does not need to keep any resources allocated at the server end. In our first example above, the object would have had to obtain access to a database resource of some type (say an ODBC connection) at the latest at the time the Locate method was invoked. It would have had to keep that connection until it was destroyed (assuming that the object exposed Delete, Update and other such methods too). Our second example, on the other hand, can obtain a connection, use it and release it all within the context of a single method call. The object essentially becomes stateless.
Now before you fire up your e-mail client and flame me, please hear me out. I am NOT claiming that this is a good way to write a plain DCOM server. I am very much aware that the act of opening a connection is expensive and that it makes no sense to open and close one per database operation. The reason I mention it here is that this becomes a vital issue when developing an MTS application (yes, we finally are there).
MTS uses DCOM as its underlying communication protocol. Your client-side application uses an MTS-hosted object exactly as if it was a DCOM object (which it is, well, sort of -- keep reading).
You develop server-side objects that are to be hosted in MTS as ordinary COM objects using any tool that can create a COM object. Here's the surprising part: you compile it as an in-process (ie DLL) server.
WHOA, didn't I say above that DCOM servers have to be EXEs? Yes, I did, and they do. Now, what if you have a DCOM server that you want to deploy on a remote machine but that server is a DLL and you can't re-compile it (maybe you don't have the source, for example)? What would you do? The obvious way to proceed is to write another EXE server that acts as a proxy for the DLL. All that server needs to do is pass on the requests it receives via DCOM to the DLL (using plain COM) and send back the results.
Well, that's (sort of) how MTS works. Your MTS-hosted objects are impersonated by MTS -- when the client registry is consulted to find a server, the registry information points to MTS via DCOM, and MTS then manages the actual object as a DLL. The reason it is done this way is that this allows MTS to efficiently communicate with the objects it is hosting and for them to communicate efficiently with each other. It also allows MTS to work its magic in terms of managing transactions and pooling resources.
Let's just develop a little terminology to avoid confusion later on. This mixture of terminology is my own -- I have borrowed a lot of terms from my dim and distant past when it is alleged I programmed on mainframes.
An object hosted in MTS is a remote object. An object that runs on the client is a local object. We always use the terms "local" and "remote" from the client machine's perspective. A DLL server hosted by MTS (which can contain one or more remote objects) is a TPR (a Transaction Processing Routine -- any Honeywell programmers out there?). An exchange between a client and a remote object is a round trip. One or more round trips may be made in the context of a transaction, which is a unit of work that is either committed as a whole or else rolled back as a whole.
OK, so the process of developing an MTS application involves writing one or more client programs (which may or may not include local objects) and one or more TPRs that expose one or more remote objects. The client calls upon the remote objects to perform work on its behalf.
If you haven't done so already, install the Option Pack for NT4 or Win95 that includes MTS 2.0. I *strongly* recommend NT as a development platform for MTS applications. Allegedly, it works under Windows 95/98 too, but if it doesn't don't bother asking me for help -- I've never run MTS under Windows 95 and, to be honest, the thought makes me break into a cold sweat. Write yourself a little DLL ActiveX server to experiment with. A good one to start with is a simple server that exposes a single method that returns the current time. Keep it simple and concentrate on the MTS stuff first.
Here is my implementation:
Public Function Ping(ByVal Msg As String) As String Ping = "Pong : " & Msg End Function
Cute, isn't it? Just create a new ActiveX DLL project, change the class name to CPing, set its Instancing attribute to 5-Multiuse and add this code. Mark the project for unattended execution, apartment threaded. Call the project MTSPing and compile it. You are done.
What you have is a COM object. Let's make sure it works. Create a new, standard EXE project. On the form, place a textbox, a label and a button, as shown below, called txtMessage, lblOut and cmdPing.
Now, paste in the following code :
Private Sub cmdPing_Click() Dim Ping As MTSPing.CPing Set Ping = New MTSPing.CPing lblOut.Caption = Ping.Ping(Trim$(txtMessage.Text)) Set Ping = Nothing End Sub Private Sub Form_Load() Me.txtMessage.Text = "TEST PING AT " & Format$(Now) End Sub
Run the program. When you press the button, you should get back the Pong message. You have verified that your server works. I find that this is a useful first step: get your server working as a stand-alone server before integrating it into MTS. You can't always do this, but if you can you should take advantage of the situation.
Now, in order to run this under MTS, launch the Transaction Server Explorer (you'll find it under Programs | Windows NT 4.0 Option Pack). Drill down in the tree view to Microsoft Transaction Server | My Computer | Packages Installed and you will see a list of the sample packages that were installed by the Setup program. A package is just a set of TPRs that are generally accessed together. Packages become really important when it comes time to deploy an application because MTS is able to create a client install program that sets up the registry entries required to enable access all the objects in a package. We need to create a new package. Right-click in the tree on the "packages installed" item and select New | Package (or use the Action | New | Package toolbar menu thingie). You get a wizard to help you. "Choose Create a New Package" on the first screen, then type in the name "Ping" on the second one. The next screen you see is the "Set Package Identity" screen. Generally, you will not want an MTS-hosted object to run in the context of the currently logged on user -- after all, there may not be a logged on user at all, or the Administrator might be logged on. As a general rule, create a User Account that has just enough privileges to successfully run the package, and specify that User Account information on this screen. For this experiment, you can leave it as it is.
OK, now that we have an empty package, it is time for us to put something into it. Drill down into your new Ping package and then into the "Components" folder, which will be empty. You guessed it, right-click the folder in the tree, and select New | Component. You get YAW (Yet Another Wizard) to guide you. The first screen you see looks like this:
These options can be a little inscrutable but are actually simple if you can decipher them. "Install new component(s)" means to register a DLL that was not previously registered on this system before installing it into the package. "Import component(s) that are already registered" means to include in this package one or more servers that are already registered on this system. In our case, we have already created the MTSPing server and registered it on the system, so we would choose the second option. The wizard then shows us a list of registered DLL ActiveX servers on the system from which to choose. (Had we chosen the first option, we would have seen a file open dialog).
Scroll down to find your ping server, select it and press Finish. You have now set up the MTSPing server to be hosted under MTS. If you re-run your test program, you should see no differences whatsoever.
What we are now going to do is create a program that, when run on a client computer, will set up that client to be able to use the ping server from MTS. Obviously, the client must already have DCOM installed, so it must be running NT4 or Win 98, or Win 95 with DCOM installed separately.
In the Transaction Server Explorer on your server, right-click on the Ping package in the tree view and select Export. You will see this screen (ie, YAW):
Hit the Browse button and navigate to a directory where the setup files should be placed. You will need to actually name the file itself (default PAK extension), not just the directory. Go ahead and press the Export button. If all goes well, you should see:
If you now look at the location you specified, you will see two files and a directory. The two files are Ping.PAK and MTSPing.DLL. These are the files that I need if I want to import the package as a unit into another MTS system (or into a new one in a recovery situation). The directory is called Clients and in there you will find a program called Ping.EXE with the distinctive package setup icon. Copy this program to a floppy (remember them?) or onto a network share accessible from the client, then run that program on the client. You should now be able to run your test program on the client (after you install it there, of course -- MTS does not install the client software).
There, that wasn't so painful, was it? Your first MTS application. Obviously, we focused on the bare necessities only, but it really isn't that difficult. Certainly, there is a lot more to writing a REAL MTS application, but at least you now know the steps involved.
MTS manages pooled resources such as database connections allowing, for example, remote objects to re-use an existing database connection rather than opening a new one. It also pools object instances themselves. MTS will not necessarily tear down an object instance when the last reference from a client is released. Instead, it will leave it alive for a little while (3 minutes by default) so that the next client that requests that object type can be given a reference to that one. In order for this to work, however, you need to set up your objects so that they are stateless -- you create them as you need them from the client, use them and dispose of them. Preferably, you don't keep your reference to a remote object across a user interaction. This way, MTS will pool the objects and the resources that they use. This dovetails nicely with the efficiency consideration for DCOM in general as we have already seen.
MTS is able to coordinate work in multiple objects (even in different TPRs) so that they work within the context of a single transaction. In order to do this, the object(s) must be marked as either supporting transactions, requiring a transaction or requiring a new transaction. The first indicates that the object will work without an active transaction but will cooperate with any transaction that might be in progress. "Requires a Transaction" says that the object can not safely work outside of a transaction and is depending on transaction support from MTS. "Requires a New Transaction" means what it says: the object is not prepared to work within another transaction context but instead needs its own (which may be nested within another one, of course). These options are selected on the "Transaction" tab of the object's property pages in the Transaction Server Explorer:
Once you have marked an object as appropriate, calling GetObjectContext() will return a reference to the -- you guessed it -- context of the current object. There are a few things you can do with an object context, but the most common things you will want to do are:
These two methods are like Commit and Rollback. A single object just tries to do what was requested of it, and if it succeeds calls .SetComplete. If it cannot complete the task and wants to abort the transaction, it calls SetAbort.
The real power of this is when you have several objects that share the work for a transaction. The classic example as used in the Microsoft samples is a bank transaction, where a "Transfer" method of a Bank object creates two instances of an Account object, and call the Debit method of one and the Credit method of the other. Obviously, we want both to succeed or both to fail -- any other situation will result in money being created or disappearing.
The "master" object (the Bank) is the one created by the client program that exposes the Transfer method. This object would be marked "Requires a New Transaction" so that a transaction context is created.
The code in the Transfer method of the Bank object would create two account objects, say objAccountFrom and objAccountTo. It would then do this:
objAccountFrom.Debit Amount objAccountTo.Credit Amount GetObjectContext.SetComplete
Each account object would signal success or failure using .SetComplete or .SetAbort as we have seen.
What we have left out is how MTS knows that the two Account objects are part of the same transaction as the master object. This is done at object creation time. The Transfer method of the master object does not use the Visual Basic New keyword, nor does it use CreateObject. If it did, these objects would not be part of the same transaction. Instead, it would do this:
Set objAccountFrom = GetObjectContext.CreateInstance("Bank.Account") Set objAccountTo = GetObjectContext.CreateInstance("Bank.Account")
This ensures that when the two account objects call GetObjectContext to signal either SetComplete or SetAbort, they obtain a reference to the same context object as the master and therefore become part of the single transaction.
How would you mark the Account objects in terms of transactional requirements? Obviously, there is no one universally correct answer but in general they would be marked "Requires a Transaction".
Is that all there is to MTS? No, not really. You need to think about what you are doing and how it impacts on your user interface. The whole concept of allowing a user to browse a table and edit it does not mesh well with client-server systems and MTS is no exception. MTS is designed to scale up well (and it will, one day, when we get automatic server pooling and roll-over on fail). If we are going to deliver these efficiency benefits to users, we need to think more in terms of executing transactions against a data store instead of browsing a table.
Us old mainframers are feeling right at home. When I first saw MTS, my immediate reaction was "CICS for Windows". I think my first reaction was pretty much on the mark.