This method begins with a call to the MTS API function GetObjectContext, which returns a pointer to the appropriate context object. The object then submits the completed orderexactly how doesn't matter here. What does matter is that if all went well, this method's final task is to call SetComplete. If anything failed, the method calls SetAbort. I'll talk later about how these calls affect any transaction this object is part of. What's relevant here is that they cause MTS to call Release on every interface pointer it holds on the object. The object and any state that it is maintaining go away. The next time its client calls Add, MTS will transparently create a new instance of the object.
Implementing IOrderEntry in this way is entirely plausible, and an application built like this would scale reasonably well. A client could use what appeared to be the same object to create and submit many ordersit wouldn't need to create a new one each timebut the server would maintain an object only when an order was in progress. An even more scalable design, though, would be for the object to call SetComplete at the end of every method, not just when the order was submitted. For this to work, the Add and Remove methods would need to read the current order (if there was one) from disk, perform their functions, then save the order back to disk at the end of each call. Once the order was safely on persistent storage, each method could end with a call to SetComplete.
Building the object in this way would allow MTS to deactivate it after every call, making it even more scalable. The trade-off, of course, is the extra disk accesses required in Add and Remove. There are cases, though, where the increase in scalability matters more than the performance hit. It may even be desirable to save the order persistently after each change. Microsoft Merchant Server, for example, does exactly this, allowing a Web-based client to begin creating an order, lose and reestablish its network connection, then pick up where it left off.
The important point is that MTS allows both options. When designing MTS-based servers (something you will dotrust me), you must determine how to manage your state. The rule that MTS encourages is simple: call SetComplete as often as possible.
Acquiring and Holding Interface Pointers
This rule is a reasonably obvious corollary to the one just described. In a bare COM or DCOM application, it may not be a great idea for a client to create a COM object and hold onto the object's interface pointers if it has no intention of using the object for a while. Doing this requires a traditional COM server to maintain that object in memory even though it's not being used. But with MTS, holding onto references to a stateless object is freeMTS deletes the object anyway as soon as it has no state to maintain. While acquiring interface pointers to objects is still a relatively expensive operation, holding onto them is not.
Although most of the changes MTS brings to COM programming are on the server side, this is a good place to think about how MTS affects clients. I said earlier that MTS is invisible to clients, which is for the most part true. There is a subtle way, though, in which the semantics of MTS objects leak through to their clients. Imagine a Visual Basic client that's setting various properties in an MTS object. The client may think it's working with an ordinary COM objectall the language syntax is exactly the same. But suppose that after setting several properties in the object the client invokes a method in the MTS object that calls SetComplete. If this happens, the object will be deactivated, as described above, losing all of its state. If the client isn't aware this has occurred, it may be very surprised. The client might, for example, attempt to get the value of some property it has just set in this object, only to discover that that value has been reset to its default. If the client doesn't know which methods in an object's interfaces call SetComplete, it might be confused by the object's behavior. This in turn implies something else: the client must know that this is an MTS object, not just a vanilla COM object.
And that's not all. Remember COM's rule that making any change to an interface requires giving it a new interface ID (IID), a new GUID? We all know that, for example, adding a new method to an existing interface requires defining an entirely new interface with its own IID. But with MTS, it's possible to make changes to an interface's semantics that require a new IID without changing anything in the interface definition itself. For instance, in the IOrderEntry example just described, there were two possible ways of implementing the interface's methods: calling SetComplete only at the end of the Submit method or calling it at the end of every method. The definition of IOrderEntry itself would be exactly the same in both, but distinct IIDs would be required for each of these two cases.
Changing only which methods call SetComplete also changes the behavior of that interface as seen by a client. If this is done, that new interface must be assigned a new IID, even though it is syntactically identical to the earlier interface. It might be nice if there were some Interface Definition Language (IDL) keyword to indicate which methods call SetComplete, thus providing an obvious marker in the interface definition of what has changed. There isn't, though, so you just have to understand what's going on and follow the rules.
Servers and Resources
The last rule allowed clients to be lazyjust getting whatever interface pointers are needed, then letting MTS worry about efficiently managing the objects those pointers refer to. By contrast, this rule requires servers to be better citizens than they have been up to now.
Traditionally, acquiring a server resource such as an ODBC connection to a database has been an expensive operation. Accordingly, developers would write COM objects to acquire a resource early, then hold onto it for the life of the object. While this made the application run fasterit didn't need to incur the cost of acquiring the resource over and overit also didn't scale very well. ODBC connections are a finite resource on a machine, and if every object acquires and holds onto its own for the object's entire lifetime, there might not be enough to go around. But the alternativeacquiring, using, and then freeing a connection every time it was neededhas historically been too slow.
By defining the notion of a resource dispenser, MTS fixes this. A resource dispenser provides an efficient way of acquiring and freeing shared, non-persistent resources on a system. The most important resource dispenser in the MTS world (today, at least) is the one that allocates ODBC connections. Implemented by the ODBC 3.0 driver manager, this resource dispenser maintains a pool of available ODBC connectionsand since the newer ADO and OLE DB interfaces typically use ODBC to access relational databases, everything described here applies to their clients, too. When an MTS object requests a connection to the database, the resource dispenser hands it one from this pool. When the object releases the connection, the resource dispenser returns it to the pool. All of this is transparent to the ODBC clientit makes the same calls as always.
Because connections to the database aren't really allocated and freed, acquiring and releasing them is much faster. An MTS object can afford to ask for a connection only when it needs one, use it, then immediately give it back. The performance penalty of doing this is greatly reduced by
the resource dispenser's connection caching. An object might, for example, request a database connection at the beginning of each method, use it, and then release it. Writing objects that behave in this way allows those objects to scale much better since they can more effectively share scarce resources.
Server Roles and Declarative Security
When a client invokes a method in a COM object, that object may need to verify that the client has the right to execute that method. If the method accesses some other resource, such as a file, the object may also need to verify that the client has the right to use that resource in the desired way. COM and DCOM servers can use the methods in IServerSecurity to learn about the client, then do whatever is necessary to determine whether the client is authorized to carry out the requested function. One common technique is to have the server thread that's handling this client request impersonate the client, then try to access the requested resource. Assuming the server operating system is Windows NT, the resource's ACL will be checked automatically, saving the programmer the trouble of doing it herself. If the access works, great. If not, the call can be rejected.
Does anybody really like this approach to authorization? I'm inclined to doubt it. This approach is complicated to
get right, and it doesn't scale well. The creators of MTS
were among the non-fans of this approach, so they pro-
vided an alternative way to let MTS objects make authorization decisions. It's called declarative security, and it's much simpler to use. (And for the hardcore among us,
you can still use more detailed mechanisms if desired,
an approach that's now known as imperative or programmatic security.)
Declarative security depends on the idea of roles. A role is just a collection of Windows NT users and/or groups assigned a particular character-string name. (Role names are unique only within a defined collection of MTS components known as a packagethey aren't globally unique.) In a banking application, for example, some obvious roles might be Teller, Manager, and Loan Officer. The MTS administrator defines which users and groups are associated with each role by using the all-purpose MTS administrative tool, the MTS Explorer.
Once roles have been defined, the administrator can also use the MTS Explorer to specify exactly which roles are allowed to access each MTS component. It's even possible to assign role-based permissions on a per-interface basis. For instance, you can allow access to an interface on some object to Managers but not Tellers. When an MTS object is executing, MTS itself will check every incoming call on every interface, determining what role the caller is in. If the caller is not in a role that has access to this object or interface, the call is rejected. Otherwise, the call is passed through to the object.
Notice what the object itself must do to make this work: nothing. You don't need any code for security in an MTS object that's using declarative security. Once an administrator has set things up, MTS itself blocks unauthorized calls.
Of course, there are some things that you just can't do this way. Suppose my object wants to allow Tellers to make transfers under $10,000, but let Managers make transfers for any amount. If I want the same code to handle both cases, I can't rely only on declarative security. Instead, I'll need to include code to check what role the client is in, then make an appropriate decision. To support this, the IObjectContext interface (the same one that includes the SetComplete and SetAbort methods) provides a method called IsCallerInRole. Using this, I can make MTS check what role the current caller is in. If the caller is a Manager, I can allow a transfer of any size. If the caller is a Teller, however, I can reject transfers over $10,000.
Roles are much simpler to use than per-user authorization checks. They're not the only option when using MTS, but whenever feasible, they're the best choice.
Server Transactions
It's probably fair to say that "Microsoft Transaction Server" is a bit of a misnomer, since much of what MTS provides has nothing to do with transactions. Instead, a large part of the MTS functionality focuses on making it easier to build scalable COM servers. But support for transactions is a requirement for many kinds of servers, and it's certainly an important part of what MTS offers.
What exactly does it mean to "use transactions"? In general, a transaction allows you to group together two or more units of work into a single, indivisible unit. For example, in the order entry scenario described earlier, a record in the inventory database must be changed for each item the client requests. For a client ordering several items, several records will be changed. If the order is submitted, all of those changes must be made permanent. If the client cancels the order, all of those changes must be rolled back, leaving things just as if the order had never existed. Allowing some of the order's changes to persist while others do not would leave the data in an inconsistent state. Rather than worrying about this itself, an application can rely on MTS to ensure that either all the changes occur or none of them do.
But waitcan't databases do this themselves? Of course they can. If you're using ODBC to access a database, for example, you can use ODBC calls to start a transaction, make your changes, then commit or roll back those changes. The DBMS will make sure that either all of the changes happen or none of them do. But what if you're making changes to more than one database and you want all of those changes to be part of a single, atomic transaction? Those ODBC calls for transactions are no help here, since they only work on a single database. And even if your object makes changes to only one database, you'll be better off letting MTS take care of transactions for you rather than relying on the database directly (and I'll explain why shortly).
So when should you use transactions? It's easy: if your COM object needs to make two or more changes, such as updates in a database, and you want all of those changes to either succeed or failpartial success isn't alloweduse transactions. If your object doesn't need to do this, then you'll still probably want to use MTS to write your server (it makes your life easier), but you won't need to use its transaction features.
Using transactions in MTS turns out to be surprisingly easy. When it's installed, an MTS component can have its transaction attribute set appropriately. You (or an administrator) can do this using the MTS Explorer. As shown in Figure 5, the transaction attribute is set using a simple dialog box. Choosing "Requires a transaction" or "Requires a new transaction" will guarantee that all data accesses made by this component will be committed or rolled back as a unit (and in some cases, "Supports transactions" will have the same effect). That's all it takesyou don't have to write any extra code to handle transactions. (To see what really goes on, though, see the "How Transactions Work" sidebar).
The way an MTS developer deals with transactions is simple. It's also quite novel, since it merges the idea of transactions with existing notions of components. Unlike traditional transaction systems where a client makes explicit calls to begin and end transactions, MTS hides transaction boundaries from clients. This is another example of how MTS strives to preserve the traditional client semantics of COM, even when transactions are being used. Even more atypically, MTS hides transaction boundaries from the MTS objects themselves.
The benefit of designing things this way is that the same component can run in its own transaction or be combined with others into a larger transaction. If each component made its own BeginTransaction and EndTransaction calls, this wouldn't be possible. By allowing MTS to automatically create transactions when required, it's possible to combine components in various ways. This allows you to create transactional applications from objects that were written by different organizations at different times, yet can still work together.
For example, imagine that I have a component that performs order entry functions, like the one described earlier, and another component that knows how to transfer money between two different bank accounts. Each component is useful on its own, and each requires a transaction (otherwise, the changes the component makes might wind up only partially done). But suppose I want to use both components in a single transaction, combining the order entry with actually getting paid for that order. With MTS, doing this is straightforward. Here's how it works.
Assume that both components have been configured (using the MTS Explorer, as shown in Figure 5) to require a transaction. A client creates an instance of the order entry component using CoCreateInstance. Since MTS intercepts this request, it can determine that this component needs a transaction, so it automatically starts one. Any changes the order entry component makes will be part of this transaction.
Now suppose that the order entry component creates an instance of the money transfer component (it can do this through a method called CreateInstance in IObjectContext). When MTS loads this component, it again notices that a transaction is required. But since the creator of this component is already part of a transaction, the new instance of the money transfer component automatically becomes part of this existing transaction. When the money transfer component completes its task, it will call either SetComplete or SetAbort, like any other MTS object. MTS takes note of this, but does not end the transaction. Instead, the transaction ends only when the order entry component, the root of the transaction, calls SetComplete or SetAbort. If both components called SetComplete, all changes made by the components will be committed. If either one called SetAbort, all changes will be rolled back.
The important point here is that the very same component binary can run in its own transaction or can be grouped with one or more other components into a single transaction. Microsoft calls this feature Automatic Transactions. It allows you to combine the traditional idea of transactions with the much newer notion of componentssomething that's essential for building component-based servers. It also explains why, even if your component only accesses a single database, you don't want to use the ODBC transaction features when MTS is available. Using ODBC transactions means your component will explicitly start and end a transaction, making it impossible to combine with other components into a single transaction. If you rely on MTS for transactions, your component and other MTS components can be combined in arbitrary ways to create transactions. Using MTS allows you to create transactional components that can be used much more flexibly.
Using transactions with components also affects how you design those components. To be most useful, each component should encapsulate some discrete chunk of work. Doing this lets you combine that component with others in arbitrary ways. Adding transactions means those useful chunks of work shouldn't span transaction boundaries. For example, think about an application that must submit items of work to a queue, then process those items. It would be entirely possible to create one component that does
both tasks, but it might also make sense to handle each of these functionssubmitting work and processing itin separate transactions. If both tasks are implemented in a single component, doing them in separate transactions isn't possible. Think about transaction boundaries before you design your componentsthe result will be more useful components.
Conclusion
The easiest way to think about MTS is to view it as just an extension to COM. This makes learning to use it easier, since COM has become an ingrained part of development. But MTS also brings a few changes to the traditional way COM programmers have done things. Those changes bring benefits, but change can also be painful. Following the rules described here will help maximize the benefits and minimize the pain.
From the January 1998 issue of Microsoft Systems Journal
.
|