OM+ is the MicrosoftÂ® WindowsÂ® 2000 runtime environment that provides a prefabricated infrastructure for solving common problems on the middle tier of a three-tier distributed environment. Like all powerful tools, it can accomplish wonderful things when properly employed and directed, but does much less and can even be dangerous when misapplied. When I wrote my book, Understanding COM+ (Microsoft Press) a year ago, I knew how to make COM+ work, but I wasn't completely sure how to get the best performance out of it with the least amount of head banging. Since then, I've learned more while dealing with many clients and students who were also using it. Based on these experiences, I've put together a list of my top 10 tips and tricks for getting the most out of COM+. Some deal with the underlying philosophy of the new COM+ environment, and others with the specific implementation issues.
I'd also like to hear the tips and tricks that have helped you. I'll be using them in future MSDNÂ® articles, books, and in my own quarterly e-mail COM+ newsletter (free, worth every penny, and available at http://www.rollthunder.com). You can send your ideas to me at email@example.com.
1. Read about Transaction Processing
Read Principles of Transaction Processing by Philip A. Bernstein and Eric Newcomer (Morgan Kaufmann, 1997). I've listed this first because I really wish that I had read this book before starting to teach myself about transactions. It would have saved me a lot of time and made me look a lot smarter. Transactions may be new to the Windows platform, but they're not new to computing. Their first industrial use was the SAABRE airline reservation system in the early sixties. The large scale of this system, which allowed many users around the world to have simultaneous access to what was then a very large database, posed problems not previously seen in the computing world. Bernstein and Newcomer do a very good job of explaining these problems, and the architectural principles required to solve them.
My students are always asking me how they can hack their way around this or that piece of COM+ that they initially don't like. For example, they want to write components that participate in transactions, but they are annoyed that COM+ requires them to use just-in-time (JIT) activation, which destroys all objects participating in a transaction when that transaction is closed. This means that they have to think carefully about managing the state of their objects. They didn't have to do it in the nontransactional world, and they don't see why they should have to do it now. Coming from the nontransactional world, they don't understand what transactional isolation is or why it is important. They are trying to practice medicine without understanding biochemistry.
Classic COM uses the Windows registry to store information describing how the operating system interacts with COM componentsâ€"for example, the mapping between an abstract CLSID and the specific server DLL that implements that class. While I found the registry initially cryptic and somewhat daunting, I understood classic COM much better once I figured out what registry entries I had to make and what they meant to the operating system.
COM+ gives you much more administrative control over your components, and therefore requires much more administrative information than does classic COM. The registry is too small and too slow to hold the new information, so COM+ uses a new system database called the COM+ catalog instead. When you use the Component Services Explorer, shown in Figure 1, you are manipulating entries in the catalog. You will find this tool is handy, but not sufficient for everything you want to do. First, it is only usable by humans, and you frequently want to read from or write to the catalog under programmatic control. Furthermore, this tool doesn't provide access to every property in the catalog, such as the collection of transient subscriptions. If you want full control over the environment in which your application will run, you must master the COM+ catalog.
Figure 1 Component Services Explorer
Fortunately, gaining mystery isn't difficult. The catalog is exposed to any interested program through a hierarchical series of system-provided objects. The Component Services Explorer is simply a separate application that provides a convenient user interface for them. Your programs can easily bypass this shell and make calls directly to the catalog.
The entry point into the catalog is an object whose ProgID is COMAdmin.COMAdminCatalog. A program that wants to manipulate the catalog creates an object of this class and calls its methods. These allow you to walk the hierarchy of objects in the catalog and set and get their properties. A hierarchy diagram is shown in Figure 2. It looks complex, but you are going to spend most of your time in the Applications/Components/Interfaces tree. Each node on the tree is an object with its own set of properties.
The best way to see the catalog properties is with the sample program viewer that comes with the Platform SDK, and is also available from MSDN Online (see Figure 3). I've selected the Applications collection. The Related Collections list shows the different collections that are available from an application. You will recognize the Components and Roles collections from the hierarchy chart. The others (RelatedCollectionInfo, PropertyInfo, and ErrorInfo) are present on almost all collections, so I left them off the chart. You won't use them very often, and a full discussion of them is beyond the scope of this article.
Figure 3 Catalog Property Viewer
The Objects list shows all the different elements in the selected collection, in this case the applications currently in the catalog. Compare it to the list shown in the Explorer in Figure 1. The Properties list shows all of the properties that describe a COM+ application in the catalog. I've selected the one called ShutdownAfter; you can see its value of 3 (minutes, which is the system default value) in the Property value field near the bottom of the window.
To demonstrate an application that manipulates the catalog programmatically, I've provided a partial listing of code from a COM+ loosely coupled event system Spy program that I wrote (see Figure 4). It displays a list of all the registered event classes in the system, and allows you to choose any of them to spy on. It then reads the type library for the event class, rolls a VTBL for it on the fly, and enters a transient subscription to that event. When it receives an event notification from COM+, it displays it in a window for you to see. A full discussion of this program is beyond the scope of this article, but it is available on my Web site at http://www.rollthunder.com.
The code in Figure 4 comes from an MFC application. In its InitInstance method, it creates the catalog object and uses the GetCollection method to open the root collection. It stores these in global variables for the use of other parts of the program. The InitDialog method is called when the user makes a menu selection that displays a list of all the registered event classes on the system. I have to iterate among all components in all the applications on the system to find each one that represents an Event class. You can see where I call GetCollection on the root to obtain the collection of applications. I then have to call a nonintuitive method, called Populate, on the applications collection. The catalog objects don't actually access the database and read the list of collections until I do this. You'd think it would be implied by getting the collection, but it wasn't written that way. Next I use the Count property of the collection to iterate over each application. Within each, I iterate over the Components collection, and so on.
3. Don't Impersonate the Client on the Middle Tier
Three-tier distributed systems generally provide access to a large back-end multiuser database. It is extremely important to make sure that the system allows each user to access only the data that she is authorized to access. For example, a human resources system might allow regular employees to see their own records, but not those of other employees. It might allow a human resource worker to see the records of all employees, but not to change themâ€"and it might allow only a few specific human resource employees to actually change records.
Your design choices about where and when in your system architecture the authorization decision is made can have a surprisingly large impact on system throughput. You have two basic choices. In the first, known as the impersonation-delegation model, the middle-tier object takes on the identity of its caller (impersonates the client) before accessing the data tier. The rules dictating which pieces of data each user is allowed to access are held on the data-tier machine. This approach is shown in Figure 5.
Figure 5Impersonation-delegation Model
This approach seems easy; you just call CoImpersonateClient on the middle tier and let the database figure it out. However, there are two drawbacks to doing things this way. First, every access requires two authentications instead of one. The middle tier has to authenticate the base client, and the data tier has to authenticate the middle tier's call. By authentication, I mean a handshake between client and server that answers the question, "Are you really who you say you are?" The server is usually configured so that authentication happens automatically when the client creates an object on it, as discussed later in this article. You don't have to write code for it, but it burns microseconds at best, and can require a network round-trip to a domain server, which burns more microseconds and exposes you to a potential bottleneck. Doing it twice is worse than doing it once.
The second problem with this approach is that when the data tier does detect an unauthorized access, it happens late in the process. You've already made a network hop from the middle tier to the back tier, often a slow one, and bothered your data-tier machine. If your operation is going to fail, you would like that to happen as early in the process as possible, so you waste little time on it. The time to discover that you forgot your wallet is when you first get in your car, not when it's time to pay the dinner check. This approach seems to me to be a holdover from the two-tier client/server days when there was no middle tier in which to put the authorization logic.
Figure 6 Trusted Server Model
A better approach for modern three-tier systems is shown in Figure 6. In this model, the middle tier performs all necessary authorization checks as part of its business process, and does not make a call into the database before it has determined that the particular client is indeed authorized to view the information it has requested. The middle tier server runs with one particular identity; the data tier grants this identity access to anything it asks for, hence the name "Trusted Server" model. You trust your wife to go through your wallet, so you don't ask her what she needs the money for. In this case, there is only one authentication, when the middle tier authenticates the base client. And when an operation fails, it does so early, without wasting time and resources.
4. Avoid Thread Affinity
COM and threads have always coexisted uneasily at best. Windows NTÂ® 3.1 didn't support 32-bit COM at all; Windows NT 3.5 supported COM on only one thread per process. When COM first became available to multiple threads in a process (Windows NT 3.51 and Windows 95), the problem of an object receiving concurrent calls on two different threads arose. To solve this problem, while still making the components relatively easy to write, those versions of the operating systems introduced the concept of the single-threaded apartment (STA). See my article in the February 1997 issue of Microsoft Systems Journal for more information (http://www.microsoft.com/msj/0297/apartment/apartment.htm).
Objects in an STA receive all their incoming calls from the thread that created the object. Calls originating from other threads pass through a message pump so the object receives them only on that one thread. It is a lot easier to simply mark your component as needing to run in an STA by setting its ThreadingModel registry entry to Apartment than it is to write all the nasty serialization code that it would take to run outside an STA.
The STA model makes it relatively simple to write components because the OS handles all required serialization through the message pump. But it purchases that serialization at the cost of thread affinityâ€"the component can only receive calls called on the thread that created it. This doesn't matter on a single desktop for several reasons. First, much of the work done by COM objects is UI-related, and thus window-related. Since a window receives all of its messages on the thread that created it (as does an STA object), an object created by a window would live on the thread that created the window. Since the object and the window belong to the same thread, they didn't have to go through the message pump to talk to each other. Additionally, a single desktop spends most of its time idle, waiting for user input, so the relatively few cross-apartment calls that did go through the message pump didn't get noticed.
Unfortunately, the very thread affinity that simplified your life on a single desktop is evil in a distributed system. Suppose you have an STA object on a middle-tier machine. When a client makes a DCOM call to the object, the call is first processed on the server by a random thread from an RPC pool that exists for precisely this purpose. But since the object for which the call is destined lives in an STA, the receiving thread needs to marshal it to the particular thread that owns the object's STA, wait for that thread to process the call, and marshal the results back.
This thread switching obviously takes time and costs CPU cycles, which is bad. What's less obvious, but could be much worse, is the potential for bottlenecks. The required STA thread might be busy or blocked servicing another request, in which case the client and the receiving thread are stuck waiting for that thread. Even if 100 threads and 50 CPUs are available, you're stuck waiting for that particular one. It's like having to wait in a long line for one particular bank teller, even though there are five tellers sitting at windows doing nothing but filing their nails. I'd find another bank.
OK, how do you do that? You need to write your components in such a way that they can be called by any thread. This means that they can't live in an STA. Instead, they have to live in other types of apartments, either in the multithreaded apartment (MTA, ThreadingModel = "free") that first appeared in Windows NT 4.0, or in the new neutral apartment (NA, ThreadingModel = "neutral") in Windows 2000. In either case, the incoming call from the client can proceed directly to the object from the RPC receiving thread. You don't have to wait for any particular teller, you can take the first one that's available. You'll get out of the bank much more quickly this way.
But if this object might receive calls from any thread, what about serialization? You're not getting it from the threading model anymore. Properly serialized multithreaded code is extremely difficult to write for all possible cases. Does this mean that you have to write it yourself? In Windows NT 4.0 it did, which is why few developers used that technique. However, COM+ provides us with the capability of obtaining serialization from the operating system. Objects that live in the MTA or NA can mark themselves as requiring synchronization by making entries in the COM+ catalog, as shown in Figure 7. This tells COM+ to put a policy object between the channel and the stub on the server side. This policy object uses a mutex to ensure that only one thread at a time accesses any method of the specified object, and it properly handles reentrancy. You will find more details, including a working sample app, on my Web site at http://www.rollthunder.com.
Figure 7 Requiring Synchronization
What else do you need besides serialization to write components that run in the MTA or NA? You need to make sure that you don't write any code that expects an object call to come in on any particular thread. For example, you must not use thread-local storage, because if you store some data on a particular thread, you have no idea when, if ever, you will receive another call from this thread. Some development environments use thread-local storage and are therefore unable to produce code that runs in the MTA or NA. Foremost among these is Visual BasicÂ® 6.0, but MFC also has this problem. You will not be able to write thread-agnostic code in either of these, thus components developed with them will suffer from the thread affinity bottleneck.
Visual C++Â® can produce thread-agnostic components using ATL. Choose the "free" threading model for the MTA when generating your object with the wizard, then change it manually in the code to "neutral" if you want the NA. It is said that Visual Basic .NET, as well as C# and the other languages in Visual Studio .NET, will be able to produce thread-agnostic code, and thus produce components that can live in either the MTA or NA.
5. Keep Transactions Short or Use Compensating Transactions
Keeping transactions short is probably the number one thing you can do to boost throughput of a multiuser transactional system. Remember that transactions must be isolated from each other. This means that objects currently working on Transaction A cannot see any of the data being used by other objects working on Transaction B. The reason for isolation is that neither transaction knows whether the other is going to commit or abort, so they don't know whether the data in the other ongoing transaction is good or not. Since you don't know if the data is good, isolation means that you can't be allowed to see it for fear you would misuse it.
Databases and other resource managers provide locks to enforce transactional isolation. When a transactional object first accesses an unlocked database record in a COM+ resource manager database, the database activates a lock so that no other transaction can access that record. The lock granularity, by which I mean the amount of nearby data that has to be locked in order to isolate the requested record, varies from one database to another and is usually configurable within the database. If another object tries to access the locked record (or anything else in the locked region), the database checks the transaction to which the requesting object belongs. If it's the same as the owner of the lock, the database allows the access. If not, the second object has to wait for the first object to release the lock. The longer an object holds a lock, the greater the potential for backlogs and bottlenecks.
Lock times cause real trouble when the business operation causing the transaction involves humans doing things at human speed, like typing in a credit card number. For example, I recently bought concert tickets from an online ticket vendor. To order tickets, you choose the number and type of tickets you want, and the computer does a database lookup to determine which seats are available. Your purchase decision depends on how much you like those seats. It is going to take you, or any human, some amount of time to think about the offer, decide whether to take it, and type in your credit card information and delivery instructions. During this interval, they can't release your seats to the next guy who asks; they're yours if you pay for them.
It would seem necessary to put some kind of lock on the arena while you are making up your mind, so no one snags your tickets. On the other hand, keeping anyone else from booking tickets for that event while you dither over left or right orchestra would represent a terrible bottleneck. No matter how decisive and quick-typing you are, this amount of time is enormous compared to computer processing times. And the overhead of locking each seat in the database would be prohibitive. Another solution that gives you a reasonable amount of time to purchase the tickets, and still lets other people buy the seats that you aren't considering, is needed.
A better approach to this problem would be to use a compensating transaction. This is an equal but opposite transaction that undoes the work of a previous transaction. It's often a good alternative to unacceptably long or indeterminate lock times. When you ask for tickets, the ticket system performs one transaction that takes them out of the general pool and marks them for your use. The ticket database has to be locked for the amount of time it takes to remove the tickets, which is done at computer speeds, so it isn't bad. If the throughput still isn't acceptable, you can have one lock per arena section, or whatever the optimum granularity is. Once this operation completes, anyone else can purchase other available tickets while you are thinking about yours. If you choose not to buy the tickets offered to you, the system performs another transaction, the compensating transaction, that puts them back in the pool for the next customer.
The one thing to beware of with compensating transactions is that the isolation level of the overall transaction, the purchase of the tickets by all parties, is not complete. While you are pondering the purchase of your specific tickets, someone else who looks at the arena won't see those tickets even though they might not be yours yet, and indeed might never be if you decide not to purchase them. A show that claims to be sold out might have more tickets in a few minutes when a user abandons his choice and a compensating transaction is applied. A later customer might get tickets that an earlier customer, who gave up in disgust, could have had. This small sacrifice in equity and determinism pays off in better throughput, and that's usually what you care about most. If it took five minutes to sell each pair of tickets, it would take roughly 34 days to sell out one event at Madison Square Garden, and that assumes 24-hour, 7-day-a-week queuing.
6. Stress Test Before Deployment
COM+ systems care about throughput, and nothing kills system throughput faster than a bottleneck. Think how often you have to wait in a traffic jam for an hour to get past a hundred-yard stretch of highway that narrows from three lanes to two before spreading out to three again (Boston's Central Artery has several examples of this). All the gazillions of dollars spent to improve traffic throughput are wasted because of this one little stretch that only comes into play above a certain load factor.
Bottlenecks are evil, but how do you find them and get rid of them in your application? Some of their common sources are known and understood, and their fixes documented, as I've discussed in the last three tips. I guarantee that your app contains others. And I guarantee that they aren't where you think they are; if they were, you would have found them and fixed them already. No matter how good an architect you are, you won't find them until you generate enough volume to hit them, at which time they will seem painfully obvious. Either you can do this yourself in your test suite (put one of these in your project's initial budget; it's cheaper than a full-time bodyguard or a new identity), or you can ask your customers to find the bottlenecks for you. Which do you think is going to help your bottom line more?
You would think this would be obvious to any developer and manager. But you would not believe the number of guys that I've seen tearing their hair out when their high-volume customer found a bottleneck that they shouldn't have had to deal with. They often have to go back to re-architect the entire design because the bottleneck is down at a low level in the system that many higher layers depend on.
Stress testing is often left until the end of the development process, as part of the final performance tuning after the development instrumentation is removed from the project. That's very bad. Sometimes it is omitted entirely to meet a shipping deadline. That's not just worse, it's suicidal. As soon as you lay out a block diagram architecture, you should run a quick stress test with empty objects to see if the infrastructure is even close to handling the projected load. If not, you'll have to rethink your division of labor, and the time to do that is when you are moving around sketch boxes instead of reams of code. Then, make sure to keep on testing as part of the regular QA process as development progresses. My e-mail today contains a desperate plea from a prospective client who's a month away from deployment on an 18-month project. His application's performance is lagging way behind the performance that's needed, and he doesn't know why or where the design went wrong. If performance testing had been done early and often, they wouldn't have to pay my "panicked client" rates.
Write some test programs that load your system; it isn't hard. Put some real humans in front of your system to see how they use it. I guarantee it's not the way you thought they would, and that will probably change your performance budget. Bang it as hard as you think you're going to get in production, then bang it twice as hard. See if you're even close. Often you're not.
A final word of caution: don't restrict your stress testing to software. I know that's what you, as a programmer, think about most, but your program is just as useless if you can't get your incredibly cool algorithms right in every obscure case as it is if a 25-cent fuse blows and you don't have a spare. Most programmers don't think about hardware. But make sure someone does.
7. Put State Where it Belongs
Arguments about stateful versus stateless components get me angry. Some people say COM+ forces you to be stateless. Others say it doesn't unless you want it to. Let's get one thing straight: COM+ does not force your objects to be stateless. The concept of a transaction, particularly its requirement for isolation, does force you and any other developer of transactional code on any system to think very carefully about where your objects' state should live.
Figure 8 Turning on JIT Activation
Transactional components in COM+ are required to use JIT activation. Nontransactional components may use it if they want to. You turn on JIT activation by selecting the appropriate setting in the COM+ catalog, as shown in Figure 8. The operation of JIT activation is illustrated in Figure 9. When the client creates a JIT object, COM+ creates a proxy, channel, and stub, just as in classic COM. When the object's transaction completes, the actual object on the server side is destroyed in the middle example (or placed back in the object pool, as outlined in Tip 10 of this article). The client doesn't know this because his proxy, stub, and channel don't change.
Figure 9JIT Activation
When the client makes another call on the object, the operating system creates a new object of the same class just in time (or fetches one from the object pool, if that's enabled), attaches it to the stub, and makes the call on it as you see in the bottom example. The object that receives the second call from the client is a different instance of the object than the one the client initially created. It will therefore not contain in its member variables any information placed there by earlier calls from the client. This is what people mean when they say that JIT objects are stateless.
This is important for transactional isolation. Objects participating in COM+ transactions never find out if their transaction commits or aborts. Suppose you had a balance of $1000 in a bank account stored in a COM+ transactional database, and suppose that as part of a transaction an object subtracted $200 from it. Suppose now that after the transaction completed, the object remembered your balance ($800) in a member variable. That balance would be correct if the transaction committed, but not if the transaction aborted. Your object knows whether it was happy, but doesn't know whether the other transaction participants were happy, so it doesn't know if this value is good or not. Rather than allow the object to contain potentially invalid information, JIT destroys the object to maintain transactional isolation.
Even though the object is destroyed, your bank balance still exists in the underlying database. And it's correct there whether the operation committed or aborted; that's why you bought the database and that's why you use transactions. So the state of the world hasn't disappeared; it's just been stored somewhere other than your object's member variables because the designer of this hypothetical system thought about it carefully and designed it well. That's what I mean when I say that arguing about whether COM is stateful or stateless is dumb. Thinking carefully about where the state is and where it ought to be is smart.
The notion isn't new. Since the beginning of COM, an object's state and behavior could, and frequently did, live in different places. For example, Microsoft Excel, the world's first embedding OLE server, keeps the state of an individual object in the object container's document file and its behavior in the Microsoft Excel server .EXE. When a client creates an object of that class, say, to support an in-place editing session, Microsoft Excel initializes the object's internal state from the state stored in the document file. An HTML page containing an ActiveXÂ® control probably keeps the state of the control, its background color, and so forth, on the HTML page and feeds it to the control on creation. State has always lived wherever a programmer decided it was cost-effective to put it.
The state that an object uses in carrying out its work can live in four different types of places, as shown in Figure 10. The characteristics of these locations vary. First, the state can live on the client and be fed to the object in a method call. For example, when manipulating a bank account, the client might pass the account number and amount to the object as parameters. This is a good location for state that originates on the client anyway. It can live for as long as the client needs it.
Second, state can live in an object's member variables. It's easy to program, it's fast to access, but it can only live for the duration of a transaction. That's a perfectly fine place to store state that an object needs to hold from one function call to another within the same transaction.
Third, state can live in some central, nontransacted location, such as the shared property manager or a static variable in a special nontransaction object class. Suppose you wanted a component that generated serial numbers for bank account transactions. You'd have to keep the last serial number issued in some central location so that all transactions could access it. It's easier to program than a database, but it won't recycle a serial number if the transaction to which it's issued aborts.
Finally, state can live in a full resource manager database. It's more difficult to program and slower to run, but it will stay consistent no matter what happens. There are many places that state can live, each with its own set of advantages and drawbacks. It is up to you to pick the optimum location for the state of your objects.
But wait a minute. Isn't the combination of state and behavior the essence of an object? Doesn't separating the two mean that you aren't programming objects any more, and isn't programming with objects a Good Thing? Don't bother me with religion's semantics, I've got a product to ship.
8. Encrypt Everything that Goes onto Your Networkâ€"All the Time, No Matter What
The question of security doesn't often arise for a single desktop PC. If you physically secure your box and its removable media, you're reasonably safe against anything short of a full-scale NSA attack. However, once your data travels anywhere on a network, the problem changes drastically. Unless you can physically secure every inch of wire in your network, and almost no one can, anything you put on a network could wind up on the front page of tomorrow's USA Today (or The National Enquirer, depending on content).
Recently, while waiting in my doctor's exam room, I noticed an Ethernet port on the wall. I was next headed to a client, so I had my notebook PC in my pack with its network dongle and a network monitor program. I said to myself, "I wonder what sort of encryption scheme they use? If any." Because they hadn't physically secured their data lines (not unusual, no one can), I could have recorded every packet sent over that network, which served a large teaching hospital. If they weren't encrypted, I could have read them all. If I had been a little less sick or a little more cranky, I'd have done it.
Medical institutions are paranoid about privacy, as they should be. For example, some receptionists now call patients from the waiting room by number instead of name, so as not to reveal patients' names to the other patients. Locking the front door like this while leaving the back door open for any moderately savvy person to suck out every network packet sent in the entire hospital would be incredibly stupid. It doesn't even take a PC to record network packets anymore; a mid-range handheld PC would do the trick. Plenty of people carry these devices around, so unless they want to strip search every patient for possible recording apparatus, the doctor's geeks have to encrypt every byte of data, all the time.
Your project has the same problem, whether you realize it or not. All data is sensitive. Try asking an airline employee if someone is on a particular flight. They won't tell you because the data is private. And it's not just about protecting data against outsiders, what about the salaries and performance reviews of your co-workers? The resignation letter written in anger and not yet sent? All data is sensitive. All of it.
How can you protect your data? It's actually fairly simple for COM+ applications. Each COM+ application sets what is called an authentication level. This administrative setting tells your computer how hard to work to determine whether the user really is who she says she is, and that the data she's sending hasn't been tampered with or eavesdropped on. You set the authentication level with the COM+ Explorer, as shown in Figure 11. The different levels are summarized in Figure 12.
Figure 11 Setting Authentication Level
The one you want is Packet Privacy. It automatically encrypts everything that application sends over the network without any more thought on your part. When this is available at the click of a mouse, failure to use it in a production environment constitutes malpractice. Do you want to sit in the witness stand, being cross-examined by a plaintiff's lawyer, saying, "Tell us in your own words, please, Mr. Jones, why you didn't set your authentication level to Packet Privacy? You read David Platt's article, didn't you? Tell the jury why you chose to spill my client's beans because you were too lazy to click your mouse!"
The main drawback to Packet Privacy is that it can be somewhat slower than other levels that do not perform encryption. By default, it uses software to encrypt the data that goes onto the wire. Using dedicated encryption hardware would be a lot faster, as it is for floating-point operations and video manipulation. Fortunately, such hardware is readily available off the shelf for not much money. Encrypted network cards contain onboard processors that work with Windows 2000 to offload the encryption task from the main CPU. They're cheapâ€"around $100. This is also a good solution for non-U.S. companies that can't currently buy the strong software encryption version of Windows 2000. They can usually buy foreign-produced encrypted network cards for not much more money. It's easy. It's cheap. You can't afford not to do it.
9. Avoid Time and Order Dependencies
COM+ Queued Components (QC) are a very handy and convenient way of using COM over Microsoft Message Queue Services (MSMQ) for asynchronous communications. See my article "Building a Store-and-Forward Mechanism with Windows 2000 Queued Components" in the June 1999 Microsoft Systems Journal for a full description of QC.
As handy as QC is, and as useful as it is in many situations, I see many clients getting into trouble by trying to put it in places it was never designed to go, and to do things it was never designed to do. Since it looks so much like classic COM from a programmer's viewpoint, they design it as if it was classic, synchronous COM.
The key point to keep in mind when designing with QC is that you never know when the server is going to get around to processing a packet of calls sent by the recorder on the client side. Think of it as e-mail for COM calls. Just as you don't know when the recipient is going to get around to reading your message, or don't know when they'll answer it, so you don't know when the server is going to dequeue and play back your recorded COM calls. You hope that it is soon enough for your business purpose, but you don't have any way of knowing for certain. You have to design with this limitation in mind.
For example, suppose a client sends a packet of calls that has an expiration time for an offer to purchase something at a certain price. If the expiration time matters, you should build it into the object's business logic, creating a "GoodUntil" property. Write the server processing logic to check this property and only proceed with the purchase if the offer is still good at the time it is processed. If not, the server can note the fact and track the message spoilage rate over time. If the client absolutely has to have a response from the server before moving on to its next operation, then QC and MSMQ are probably the wrong approach; you should use synchronous DCOM instead.
In a similar fashion, you never know the order in which recorded packets of QC calls are going to be processed on the server. First, since they can come from different clients on different parts of your MSMQ network, you never know the order in which the messages will even arrive on the server. Again, think of e-mail. Different messages can, and frequently do, take different routes. It's not at all uncommon for an earlier one to be held up while a later one scoots by on a different pathway.
Even when messages do arrive on the server side, it is very difficult to enforce any particular order of processing. The messages will be dequeued from MSMQ in the order in which they reached the queue. However, the QC player service is multithreaded to increase its throughput, which is usually what you care most about. Once a message is dequeued, essentially anything can affect the running of separate threads. Perhaps the processing of the first message to be dequeued has to wait for the result of a long database search, but the processing of the second one doesn't. Processing of the second message could very easily begin, and for that matter end, before processing of the first message has completed. You have to take this into account when designing your QC objects. Basically, you have to design each component so that a client's session with it is self-contained, having no dependencies on any other QC objects.
Order dependency is especially sneaky when you combine QC with the COM+ loosely coupled event system (see my article "The COM+ Event Service Eases the Pain of Publishing and Subscribing to Data" in the September 1999 Microsoft Systems Journal). Firing events often implies an order: "Hey this thing happened. OK, now the next thing happened." For synchronous delivery with DCOM, this is perfectly reasonable. However, with QC, there is no way to be sure that the order in which the incoming events are processed is the order in which they happened. My favorite example is the nurse using a handheld PC that finds a patient dead in his hospital bed. She clicks the button that causes a QC event to be sent, noting that the patient in Room 330 has died. Later on, a new patient gets admitted to Room 330 and the hospital census app sends out a QC event announcing the fact. If the mortuary attendant receives them in the wrong order, well, it could get a bit sticky.
10. Use Object Pooling for Speed
The locker room of a winning team is always a fun place to be. When I visited Microsoft in March to research this article and my next book, the COM+ team was still celebrating a top performance in the latest transaction processing throughput benchmarks. The nonprofit Transaction Processing Performance Council develops these benchmarks and periodically posts the results of running them. You will find their Web site at http://www.tpc.org.
In February 2000, Microsoft managed to process 227,000 transactions per minute running Windows 2000 on an absurdly powerful Compaq server, using Microsoft SQL Serverâ„¢ and COM+. The closest competition was IBM/Oracle with 136,000 transactions per minute on a system costing almost twice as much. (In October, Microsoft and Compaq submitted new TPC-C tests boasting over 50,000 transactions per second.)
The COM+ team members explained to me how they had accomplished their speedâ€"by using object pooling to recycle database connections, instead of having to open and close them every time. Your applications can use this technique as well to improve performance. But it only works in certain cases, and you have to think carefully about how you will employ it.
Figure 13 Enabling JIT Activation
You mark a component as pooled by checking the box in the Component Services Explorer, as shown in Figure 13. When your application starts up, COM+ will immediately create the number of objects that you specify as the minimum and place them in the pool. When a client creates an object of the specified class, COM+ will fetch one from the pool instead of creating a new one. As clients create more objects than the specified minimum, COM+ will create new ones up to the specified maximum. When the object is released on the server side, either because the client released it or from JIT activation, COM+ returns the object to the pool instead of destroying it. Figure 14 shows the Component Services Explorer displaying object pooling statistics.
Figure 14Pooling Stats in Component Services Explorer
You might think that object pooling is faster because it saves the overhead of object construction, but this isn't really the case. I ran a benchmark with a bare ATL object in both pooled and nonpooled configurations, and saw no difference in the time of construction versus the time of fetching from the pool. The real-time savings comes when an object has to initialize itself in an expensive way, the results of which can be reused among clients. For example, an object might have to read the entire list of foreign exchange deals closed during the previous week to support its decision-making capabilities. Or it might want to connect to external resources, such as a database, which require expensive authentication for security purposes (see Tip 3). You have to be careful, though, that the information you reuse does not contain anything that would break transactional isolation.
A component that you want pooled must be developed to run successfully in this environment. The most important rule is that it be thread-neutral, that it not care which thread it receives its calls on. That's okay, because you want to do that anyway as explained in Tip 4. Pooled components must also support aggregation because they will be aggregated with an interface containing administrative information used by the pool manager. Pooled components also should not use the freethreaded marshaler.
A pooled component usually wants to support the IObjectControl interface, which has the methods Activate, Deactivate, and CanBePooled. COM+ will query for this interface and call its methods to inform a pooled object of significant events in its lifetime. COM+ will call the Activate method when it fetches the object from the pool and assigns it to a context. The object uses this method to do any sort of client-specific initialization that it requires, for example, setting its member variables to their default values. This is also the first chance that an object has to obtain its context, and many objects will fetch it and store it locally at this point. COM+ calls the CanBePooled method when the object is about to be placed back in the pool. The object returns TRUE if it is able to be recycled back into the object pool, or FALSE if it is so badly damaged that it needs to be destroyed. COM+ calls the Deactivate method when the object's session is finished, whether it is going to be destroyed or placed back in the pool. This is the object's chance to release any client-specific resources it may have acquired.
COM+ is a much more powerful runtime environment than anything else that's ever been deployed on a PC platform. Like all powerful tools, it requires careful attention to achieve the best performance. I've just discussed some of the tips and tricks that I've learned from people in the trenches. I hope they help you.
The author would like to thank Dick Dievendorff and Joe Long of the COM+ team for their contributions to this article.