At this point, you should have a pretty good idea of the services provided by MTS. MTS specifies some rules that components and applications must follow in order to use these services. In addition, the behavior of the services has a definite impact on how application servers should be designed. We'll discuss the general concepts of the programming model here and the specifics in Part Two.
The application server programming model is based on two fundamental ideas:
Most developers are not experts at component-based design and database theory and transaction processing theory and multi-threading and networking and everything else that makes up the typical corporate distributed application. A key goal for the application server programming model is to make it possible for a developer to contribute to application server development without expertise in all these areas.
MTS accomplishes this goal by providing the infrastructure services required by all application servers. The programming model strongly encourages developers to follow its rules and let MTS take care of the plumbing. The basic COM programming model offers a lot of options for creating components; the application server programming model specifies which options to use for components that run within the MTS environment. These options correspond to the options best supported by the widest range of COM-aware programming tools, as listed here:
This correspondence means that a senior developer or application designer can hand a developer on the project team a specification for a component and the developer can use wizards or other assistants provided by his or her programming tools to create the skeleton of the component with very little effort. The developer can then focus on implementing the business logic of the component. For many components, the developer will not need to worry at all about the fact that the component will be used in a distributed, multiuser environment. As developers become more familiar with the distributed application environment, they can take on responsibility for more complex components and design issues.
Resources used by application servers are limited: only a finite amount of memory, processing power, network bandwidth, database connections, and so on is available for use. Dedicating resources for individual client use is not very efficient because most of the time the resources are just sitting around waiting for the client to do something. Like get back from lunch…
Application servers achieve scalability by sharing resources. As we've seen, MTS provides the infrastructure support for resource sharing. However, MTS needs some help from the objects it manages to determine when it is safe to discard or recycle resources that are in use. The programming model defines five simple rules for how applications and components should be written to share resources most effectively, listed here:
Rule 1: Objects should call SetComplete as often as possible.
Rule 2: Base clients should acquire and hold interface pointers.
Rule 3: Objects should acquire resources late and release them as soon as possible.
Rule 4: Application servers should use role-based declarative security.
Rule 5: Application servers should use transactions when appropriate.
As mentioned, clients don't really hold interface pointers to objects in the application server process—they hold interface pointers to context wrappers generated by the MTS Executive. MTS uses the context wrapper and its associated object context to perform JIT activation and as-soon-as-possible deactivation of objects running in application server. JIT activation improves scalability by reclaiming resources associated with objects that no one is actively using.
The MTS Executive cannot determine on its own when it's safe to deactivate an object. The object needs to tell MTS when it can be deactivated, and it does this by calling SetComplete or SetAbort. Calling one of these methods helps ensure that transactions and resources are not held for long periods of time.
SetComplete and SetAbort are two methods of the IObjectContext interface exposed by the object context, as shown in Figure 4-5. The object context keeps track of information about the object, such as the activity it belongs to, the transaction it belongs to, and its security identity. IObjectContext is the primary way objects communicate with the MTS Executive. A component can access an object's object context by calling the aptly named GetObjectContext method, which returns an IObjectContext interface pointer. Once it has the interface pointer, the component can call either the SetComplete method or the SetAbort method to indicate that it has finished its work, and the object's state and resources can be reclaimed. SetComplete is called if an object wants to indicate that it has completed successfully and votes to commit any transaction it is enlisted in. SetAbort is called if the object votes to abort its transaction.
Figure 4-5. Using the object context.
You might be wondering what's meant by "call SetComplete as often as possible." This notion turns out to be one of the key design points in building application servers. Conceptually, when an object is deactivated everything about that object stored in memory in the application server process disappears. Completely. When an object is reactivated, it must be treated exactly like a newly created object. Clients cannot make any other assumptions about the state of the object. So calling SetComplete "as often as possible" means calling it whenever it is safe for the object's in-memory state to disappear.
In the extreme case, SetComplete can be called at the end of every method implementation. Objects exist only while being called by a client. Components that are written this way are known as stateless components because they retain no per-object state in memory between method calls.
Many people believe that MTS requires developers to write stateless components. This is not true. MTS provides services to help developers manage state effectively. Stateless components are the right approach for many situations, but not for all. The state information needs to be kept somewhere, after all. If retrieving the state from wherever it's stored is more expensive than keeping the object in memory between method calls, a stateful component might be more appropriate.
When you are deciding what approach is appropriate for a specific component, it's useful to think about the places state can be kept and the cost of accessing the state from an object. The four basic types of state are client-managed state, per-object state, shared transient state, and persistent state.
Client-managed state Client-managed state is information kept by the caller. Objects don't really care about any client-managed state, except when it is passed to the object as parameters of a method call.
State that's kept on the client doesn't need to be remembered on the application server, freeing up server resources. However, there are transmission costs associated with moving the state back and forth between the client and the object. Transmission costs depend on how large the state is and the relative locations of the client and object.
In most cases, state that's transmitted to the object as method parameters does not need to be stored in the object at all. The method implementation simply uses the parameters directly.
Per-object state Per-object state—what developers with an object-oriented background think of simply as "state"—consists of the data members of the object defined by the object class implementation. One of the reasons stateless objects cause so much consternation is that the whole concept of an object without any data members isn't very meaningful in the object-oriented world. Don't panic. You can have per-object state in MTS-managed components—you just have to be careful how you use it.
One issue with per-object state is that it is transient. If the system fails, the state is lost. A larger issue, however, is that as long as the state must be maintained, MTS cannot deactivate the object and reclaim its resources. These resources go beyond the memory consumed by the object itself.
For example, the MTS environment provides thread management and concurrency control services to objects. This lets developers write components as if the components will be used by a single client at a time, yet still build scalable application servers. MTS 2.0 maintains a pool of up to 100 STA threads for each MTS-managed server process. When a client calls into the process, MTS picks an unused thread from the pool. The apartment associated with this thread will be used for all objects activated in the process during the current activity. So far, so good. But what happens when all the threads are in use?
As it turns out, MTS 2.0 starts picking from used threads once the pool is consumed. Remember that in a single-threaded apartment, access to all objects created in the apartment is synchronized, so at most one object executes concurrently. This is fine for objects in the same activity. By definition, MTS ensures that calls within the activity are serialized anyway. However, if objects from multiple activities are sharing a single apartment, all but one of these activities will be blocked at any given time. This limits performance and scalability of the application server.
What does this have to do with per-object state? Remember too that an apartment threaded object is tied to the thread it was created on for its entire lifetime. If you are maintaining per-object state and your object cannot be deactivated, your object is tying up a thread from the thread pool, increasing the chances that threads will need to be used by multiple concurrent activities.
Using per-object state can also complicate transactions. MTS does not let you maintain per-object state across transaction boundaries. As long as a transactional object does not call SetComplete, its transaction cannot commit. The transaction can time out, however, causing the transaction to abort. If you have components that need to participate in transactions, you need to think carefully about using per-object state.
Shared transient state Shared transient state is state in which process-wide information is stored. It is useful for state that must be shared across transaction boundaries or by multiple clients but that can be lost if the system fails. (Think global variables…)
One difficulty with using shared transient state in a multiuser environment is that the state must be protected from concurrent access. MTS provides the SPM to help manage shared transient state. The SPM ensures that shared state can't be read while it is being updated and that only one activity can update the shared state at a time. We'll look at the SPM in detail in Chapter 9.
The cost of using shared transient state is primarily the memory cost of storing the state. Some overhead also occurs in locating the desired state. Shared state can create a performance bottleneck if state is frequently updated or updates take a long amount of time, since access is serialized.
Persistent state Unlike other types of state, persistent state survives system failures. Persistent state is stored in durable storage, such as a DBMS, and exists whether or not the client application or application server is executing.
Keeping persistent state consistent in a distributed, multiuser environment is a complex task. As we've seen, transactions are the standard technique for coordinating updates across multiple data stores and multiple users.
Several costs are associated with persistent state. First is the cost of accessing the data store. Second is the cost of any transactions. And last is the cost of copying data into memory. For many applications, any performance costs are more than offset by the benefits of durable storage and transactional updates. Of the four types of state, persistent state is the only type of state that is rolled back on aborted transactions.
Most distributed applications will use all four types of state at some point. The tricky part is figuring out which type to use when. We'll come back to this issue in Chapter 7. For now, the key thing to remember is that calling SetComplete as often as possible helps MTS manage application server resources efficiently.
Let's turn our attention for a moment to the clients calling into application servers. In MTS, any code that calls an MTS-managed object is called a client. A client that is not part of an MTS-managed process is known as a base client.
If a base client might make many calls to the same type of object, the client should acquire the interface pointers it needs and hold onto them. The reason for this is simple: base clients and application servers normally run on separate machines. Locating the correct server machine, negotiating the network protocol to use for DCOM, establishing a connection, and getting the interface pointer is a relatively expensive process compared to holding onto an established connection. JIT activation on the server ensures that resources aren't tied up unnecessarily if the object isn't being used.
Clients that hold interface pointers do need to be aware of how the objects they're calling manage state. Stateless objects aren't a problem. The client will pass any information needed by the object as method parameters. Each time the client uses the object, it acts like a completely new object. Stateful objects can cause trouble, however. The client needs to understand when the object state might be reset, as well as any restrictions on the order in which methods are called. This behavior should be documented as part of the interface definition, and once published it is as immutable as any other part of the interface definition.
Within the application server, the rules are a little different. The goal here is to share resources as much as possible. Most resources need to be shared because they are scarce. For example, a database server might be licensed for a certain number of concurrent connections. Once the connections are gone, incoming requests will fail until a connection is freed up. The less time an object holds onto a resource, the more likely objects will be able to acquire the resource when they want it.
One potential drawback to this approach is the cost of acquiring resources over and over again. Initialization costs can be quite high for things like database connections. The cost can be reduced by pooling resources once they've been initialized. After an object has finished with a resource, the resource can be placed into a pool. The next time an object needs to acquire a resource, it looks in the pool first and creates a new resource only if necessary. For some resources, pools can be populated by background threads while the application server is idle, further reducing the apparent cost of acquiring those resources.
As we've seen, in MTS automatic resource pooling is provided by DispMan and resource dispensers. DispMan works with the MTS Executive to ensure that resources are reclaimed when objects are deactivated or transactions are complete. Thus, at a minimum objects must not try to hold onto resources across transaction boundaries. The sooner an object releases a resource it has been using, the sooner that resource is available for reuse.
Securing access to components and resources is critical in distributed computing. In Windows NT, access to resources is controlled by access control lists (ACLs). Users are authorized to use a resource if their user ID appears in the ACL with the permissions needed for a particular operation. For example, John might have read-only access to the Employee Records database, whereas Mary has read/write access. As we saw in Chapter 2, COM extends this user/resource model to support components.
It's tempting to argue that this security model does not scale well. Applications consist of many components accessing many resources. As the number of users increases, so does the burden of maintaining all the ACLs. If individual user identities must be used to run components or access resources, objects and resources cannot be shared across users, severely limiting the number of users who can be supported by the application server.
This argument rings false, however. Certainly the COM security model permits applications to be developed and deployed in such a way that resources cannot be shared and maintenance is a nightmare, but it does not force applications to be developed or deployed this way. Windows NT groups can overcome much of the administrative burden of maintaining ACLs for components. In addition, in the three-tier application, users do not directly access resources—components do. Components can be configured to run as specific user identities, and only those identities need access to resources used by the components. If the components are running as a specific identity, objects and resources can be shared.
The real argument against COM security is not that building and maintaining scalable applications is impossible, just that it is harder than it ought to be. The COM security model is very much a physical model, closely tied to implementation details. To use it effectively, developers and administrators need to worry too much about plumbing. Developers generally need to write some code to manage call security, and the code is hard to get right without a good understanding of COM security. Developers must also understand how the underlying security providers and remote procedure call (RPC) authentication work, or application development and testing can quickly become a frustrating exercise in trial and error. In addition, system administrators need to understand too much about COM security and RPC authentication in order to use the DCOM Configuration (DCOMCNFG) tool during application deployment.
Enter MTS. MTS defines a new conceptual model for security, based on roles and packages. With MTS role-based security, developers and administrators can focus on the big picture—who should be allowed to do what with the system—rather than the plumbing required to secure their applications. Generally, developers do not need to write any security-related code. They simply declare the type of security they want, and the MTS Executive takes care of the details. Thus, role-based security is sometimes called declarative security or automatic security. Of course, there are some situations in which security decisions rely on information only available at run time, so MTS also provides programmatic access to security information.
Let's start by looking at authorization security in the MTS model. Authorization security determines whether a client is allowed to use a particular object. MTS performs authorization checks on calls into MTS-managed server processes, as shown in Figure 4-6. Authorization checking can be enabled or disabled for the process as a whole. If it's enabled for a process, authorization can be disabled for specific COM classes hosted by the process.
If authorization checking is enabled, MTS must determine whether individual calls to objects should be allowed. This is where roles come in. A role is simply a logical group of clients that are permitted to access objects or interfaces. Each role has a human-readable name. Roles usually map directly to real-world concepts, which can be helpful when you try to define security requirements for your customers and then translate those requirements into the application design and implementation. For example, the banking application shown in Figure 4-6 has Teller, Manager, and BankObjects roles. Managers are allowed to call objects created by component A and BankObjects are allowed to call objects created by component B. Developers can define whatever roles are needed to describe each type of client that should have access to their applications.
Figure 4-6. MTS role-based security.
Before clients can access secured applications, roles must be bound to specific users or groups. Administrators populate roles during application deployment, using the MTS administrative tools. In our example, user Joe is assigned to the Manager role, and Jane is assigned to the Teller role. At run time, the MTS Executive intercepts each call, determines the caller's identity, and figures out which roles the caller is a member of. If the caller is a member of a role that is allowed to access the type of object or interface being called, the call proceeds. Otherwise, the call fails with a security error. Figure 4-6 shows that Joe is allowed to call object A since he is a Manager but that Jane is not allowed to call object A. Likewise, the Bank Objects process is allowed to call object B because it is running as the BankApp user, which is a member of the BankObjects role.
All this happens automatically, without any custom code in each component, even for relatively sophisticated security requirements like restricting access to specific interfaces exposed by an object. Design and deployment issues are clearly separated. Developers decide what type of security is needed, but they don't need to worry at all about specific user accounts or security providers. Administrators decide which users are allowed to perform roles defined by the application, but they don't need to understand how the plumbing works.
As mentioned, MTS performs authorization checks on calls into a server process. Think about this for a minute. An MTS server process hosts one or more components. These components share an address space, threads, resource pools, and so on. It seems reasonable that calls from one object to another within the process should not require authorization checks. Only calls coming from outside the process need to be checked.
This raises the question, how does MTS know what components should run in the same process? In the MTS model, components that should run together are collected into packages. A package is a set of components that perform related application functions. Packages are the primary unit of administration and trust in MTS. Components are added to packages using the MTS administrative tools. These tools are also used to set MTS-specific options for packages and their components. We'll see how to use the primary administrative tool, MTS Explorer, to manage packages in Chapters 10 and 14.
Most security settings in the role-based model are established at the package level, which greatly simplifies administration of components running under MTS. There are two types of packages: library packages and server packages. A library package runs in the process of the client that creates it. Library packages are useful for utility components used by multiple applications. A server package runs as a separate MTS-managed process. Only server packages can be secured using MTS.
If authorization checking is required for an application, a developer will create a server package and add the components to the package. The developer enables authorization checking for the package. If some COM classes in the package do not require authorization checks, the developer can disable checks on those classes. The developer then defines roles for the package. Once the roles are defined, the developer can specify which roles are allowed to access each COM class in the package and, optionally, which roles are allowed to access specific interfaces exposed by each class. At deployment time, an administrator will populate the roles defined for the package with actual users and groups.
MTS also lets developers and administrators define other process-wide security settings. The authentication level used for COM calls to each package can be specified to establish how frequently client credentials are authenticated. The package can also be configured to run as a specific user identity. This simplifies administration, since only that identity must be granted access to resources used by the package. It is not necessary to grant all clients of the package access to the resources. Running a package as a specific identity also helps with resource consumption. In general, resources such as database connections cannot be shared by clients running as different identities. This helps ensure that an unauthorized client can't get access to the resource. If a package runs as a specific identity, package-wide resource pools can be used to share resources across components and calling applications.
With role-based security, authorization checks are performed only on entry to a package and are based solely on who is making the call and what method is being called. In some scenarios, this level of security is not sufficient. For example, bank withdrawals over a certain amount might require manager approval.
MTS provides two methods on the object context, IsCallerInRole and IsSecurityEnabled, to help developers implement programmatic authorization checks. The IsCallerInRole method is used to determine whether the client making a method call is a member of a given role. IsSecurityEnabled is used to determine whether authorization checking is enabled. (If security is not enabled, it doesn't make a lot of sense to check whether the client is in a particular role.) These methods are easily used from any language and completely hide the details of the underlying COM and Windows NT security services, as shown in this snippet of Microsoft Visual Basic code:
Dim ctxObject As ObjectContext If (lngAmount > 500 or lngAmount < -500) Then Set ctxObject = GetObjectContext() If Not ctxObject Is Nothing Then If (ctxObject.IsSecurityEnabled and _ ctxObject.IsCallerInRole("Manager")) Then ' Do normal work. Else ' Report error. End If End If End If
If more detailed information about the caller is required, MTS also provides an advanced security interface, ISecurityProperty, on the object context. This interface is particularly useful for logging security violations. For most other situations, declarative security or simple role-based programmatic security checks are sufficient.
Last but not least, developers should use transactions as appropriate in their application servers. We've already looked at transactions in general and how they work in MTS. Let's take a quick look at transactions from the programming model perspective.
In the application server programming model, multiple objects work together to provide services to client applications. In general, one object used by a particular client doesn't know anything about the other objects used by that client. For example, let's say you use an online banking application to pay your phone bill. Somewhere in the application are an object that knows how to send payment information to the phone company and an object that knows how to debit your checking account. There's no reason for these objects to know anything about each other. They perform completely independent tasks.
At a higher level, the application might contain a method named PayPhoneBill that knows how to use the payment and debit objects to perform the specified task. When all the subordinate objects complete their work successfully, implementing the higher-level behavior is straightforward. Things are considerably more interesting when you start thinking about error handling and recovery. Let's say the PayPhoneBill method is implemented something like this:
Debit checking account for amount of phone bill. Credit phone company account for amount of phone bill. Send payment information to phone company.
Now, what should the PayPhoneBill method do if sending the payment information fails? One approach would be for the method to carefully code compensating actions to undo the work it has already completed. But these actions are also subject to failure, and even if they were guaranteed to complete successfully, recovering from all the possible combinations of errors is going to add a lot of complexity to the implementation.
A better approach is to use transactions. Transactions ensure that resources are not permanently updated unless all the work done within the transaction completes successfully. For example, if sending payment information fails, the transaction aborts and no money is debited from your checking account or credited to the phone company account.
As with security, MTS provides declarative and programmatic support for transactions. Most developers will find declarative transactions, or automatic transactions, sufficient for their needs. To use automatic transactions, a component developer simply uses the MTS Explorer to specify the transactional attribute for each COM class. The possible attribute values are listed in the table below.
|Requires A Transaction||If caller is enlisted in a transaction, object will be enlisted in the same transaction. If caller is not enlisted in a transaction, a new transaction will be created and the object will be enlisted in the new transaction.|
|Requires A New Transaction||A new transaction will be created and the object will be enlisted in the transaction.|
|Supports Transactions||If caller is enlisted in a transaction, object will be enlisted in the same transaction. If caller is not enlisted in a transaction, the object will not be enlisted in any transaction.|
|Does Not Support Transactions||The object will not be enlisted in any transactions.|
MTS uses the object context to pass information about transactions from clients to subordinate objects. The object context exposes the IObjectContext CreateInstance method for clients to create subordinate objects. This method should be used by components that will run within the MTS environment to create subordinate objects that run within MTS, to ensure that object context values flow correctly to the subordinate objects. Base clients can use normal COM object creation functions.
At run time, the MTS Executive will use the transaction attributes to automatically figure out when new transactions must be created and which transaction to enlist each object in, as shown in Figure 4-7. Developers call IObjectContext SetComplete to indicate when a transactional object has completed its work successfully and wants to vote to commit its transaction. When errors occur, developers call IObjectContext SetAbort to vote to abort the transaction.
When a new transaction is created, the first object enlisted in the transaction becomes the root of the transaction. A transaction exists until the root object signals it has completed its work by calling SetComplete or SetAbort. At this point, MTS uses the two-phase commit protocol to decide whether to commit or abort the transaction. When the transaction is committed or aborted, MTS deactivates all objects involved in the transaction. This technique helps enforce transaction isolation and consistency, in addition to freeing up resources for use by other transactions.
Note that transactions do not completely eliminate the need for error handling. Transactions simply ensure that persistent state managed by resource managers is updated correctly when multiple objects cooperate to implement some functionality. If a transaction aborts, shared transient state does not get rolled back. Nor does an object's call to SetAbort cause an immediate abort—the transaction continues until the root of the transaction has completed its work. So while the transaction is in progress, clients might still want to use errors returned from object method calls to decide whether to continue doing work. For example, in our PayPhoneBill method earlier, if crediting the phone company account fails, there's really no point in sending payment information to the phone company. The Credit method should call SetAbort to vote to abort any containing transaction and return a normal COM error. The PayPhoneBill method can then detect the COM error and elect to skip the SendPayment method.
Figure 4-7. MTS automatic transactions.
Using automatic transactions ensures that independently developed components can be composed into new functionality without modifying every component. In general, any components that update persistent state should support transactions. Any component that uses two or more subordinate objects to perform a single indivisible task should use transactions to simplify error recovery. If for some reason developers want to control transaction boundaries explicitly, MTS provides a generic transaction context object that can be used to do so. However, many of the benefits of the application server programming model are lost when clients use the transaction context object instead of automatic transactions. We will not discuss using the transaction context object in this book.