As you might expect, COM+ provides all the services provided by MTS 2.0. Most of these services have been enhanced in some way for COM+. Of particular interest to developers are object pooling, object constructors, compensating resource managers, and security enhancements. COM+ also adds several new services to help you write large-scale, enterprise-wide applications. These services are generally exposed via attributes and can be composed—in other words, you can pick and choose the combination of services you want to use. (In some cases, using one service will require the use of other services.) In the following sections, we'll take a brief look at the major features of COM+ that are not available in COM or MTS today.
As we saw in Chapter 8, although MTS 2.0 appears to support object pooling via the IObjectControl interface, it doesn't actually pool objects. Whenever an object is deactivated, the object is destroyed. In COM+, objects can be pooled. When an object is activated, it is pulled from the pool. When the object is deactivated, it is placed back into the pool. However, there are some restrictions on which classes can be pooled.
The primary restriction is that a COM+ class that wants to support object pooling must not have thread affinity—that is, it cannot expect to run on the same thread for its entire lifetime. COM+ introduces the thread-neutral apartment model to help developers create classes without thread affinity. (In fact, this is the preferred threading model for COM+ classes.) In this model, an object is always called on the caller's thread, regardless of whether the thread is an STA thread or an MTA thread. To support the thread-neutral apartment model, a COM+ class must not use thread local storage (a mechanism to allocate storage for thread-specific data) or other thread-specific techniques. COM+ synchronization services can be used to protect component-wide and per-object data from concurrent access, or the class can implement its own concurrency control.
COM+ classes that want to support object pooling must use the thread-neutral apartment model or the free-threaded model; they cannot use the apartment threading model. In addition, the class must implement IObjectControl and must be aggregatable. Because not all COM+ classes can be pooled, simply returning TRUE from IObjectControl CanBePooled is not sufficient to enable object pooling. You must also add the class in a COM+ application and set its pooling-related attributes. First you must enable object pooling for the class, and then you can specify the minimum and maximum pool sizes as well as the length of time a client will wait before an object request times out. Once you have set the pooling-related attributes, COM+ will call IObjectControl CanBePooled to determine whether individual object instances can be pooled at run time.
COM Aggregation is a technique for reusing COM class implementations. In aggregation, one object (called the outer object) creates another object (called the inner object) and exposes the inner object's interfaces as if they were implemented by the outer object itself. An aggregatable object can be used as the inner object during aggregation. A COM class that is aggregatable must implement IUnknown in a specific way, as described in the Platform SDK documentation topic "Aggregation."
Clients do not require any modifications in order to use object pooling. As far as the client knows, it receives a pointer to a newly created object with every creation request. However, if pooled objects hold onto limited resources or the pool size is limited, clients should release pointers to pooled objects as soon as possible so that the pool is not depleted.
Object pooling is particularly useful for classes in which creating an object is expensive. If the cost of creating an object is more than the cost of maintaining the pool, object pooling can improve performance. Object pooling can help you gain the same kind of scalability benefits you get today from ODBC connection pooling for your own expensive resources.
Object pools are maintained on a per-CLSID basis. In some situations, it might be handy to have several pools for the same basic type of object, but with different initialization information. For example, it might be useful to have several pools of objects that maintain database connections, in which each pool's objects use a pool-specific data source name (DSN) to establish the connection. A single COM+ class implementation could handle this scenario, if there was a way to specify the DSN at object creation time.
Specifying external initialization information is the purpose of COM+ object constructors. A generic COM+ class implementation supports object construction by implementing the IObjectConstructor interface. The construction string to be passed to objects of a particular CLSID that uses the class implementation is specified as a class attribute using the COM+ administrative tools. The IObjectConstructor implementation can retrieve the construction string during object creation and initialize the object appropriately.
Object constructors in conjunction with pooled objects provide an easy-to-implement alternative to full resource dispensers, which were discussed in Chapter 4. For example, ODBC connection pooling could be implemented using object pooling and object constructors. You could also use object constructors without using pooled objects. This technique is useful for situations in which you don't want to hard-code configuration information in objects. For example, in our discussion of building data objects in Chapter 8, we used a file DSN to hold information about the database to connect to. This way, administrators could adjust the DSN at deployment time without modifying the data objects. In Chapter 13, however, we saw that using a file DSN is relatively slow. If our data object supported IObjectConstructor, we could get the performance benefits of using either a system DSN or a connection string, with the flexibility of specifying the DSN information outside the data object implementation.
Object pooling and object constructors help you write scalable applications that use transient resources but do not directly participate in transactions. Compensating Resource Managers (CRMs) are a mechanism whereby nontransactional resources can participate in transactions managed by the Microsoft Distributed Transaction Coordinator (MS DTC). CRMs are implemented as a pair of COM+ components, the CRM Worker and CRM Compensator, that perform the normal work of the resource and a compensating action. A compensating action undoes the effect of normal action. If the transaction aborts, the compensating action can correct for the normal action performed by the CRM as part of the transaction. CRMs do not provide the isolation capabilities of full resource managers, but they do provide transactional Atomicity (all or nothing) and Durability via the recovery log.
The CRM Worker performs the normal action for updating the resource as part of the transaction. This action is specific to the particular type of CRM and is accessed by application components that want to use the resource via a CRM-specific interface. The CRM infrastructure defines an interface the CRM Worker can use to write records to a durable log so that recovery can be performed in case of failure. All actions performed by a CRM must be idempotent—in other words, performing the action more than once will lead to the same state. For example, setting a field in a database record to a specific value is an idempotent action. Incrementing the field by a specific amount is not.
As mentioned, the CRM Compensator is responsible for providing the compensating action. The CRM Compensator implements an interface defined by the CRM infrastructure, which calls the CRM Compensator to notify it about prepare, commit, and abort phases of each transaction. During the prepare notification, the CRM Compensator can vote no to force an abort of the transaction. The commit notification is used to clean up after the normal action performed by the CRM Worker, and the abort notification is used to perform the compensating action. The CRM Compensator can also write records to the recovery log to indicate which actions have already been compensated for. During recovery after a failure, the CRM infrastructure reads the recovery log and calls the CRM Compensator to finish processing as if no failure had occurred.
To create a CRM, you write a CRM Worker and CRM Compensator component pair. These components are typically installed in a COM+ library application, so they are available to multiple server applications. A server application that wants to use a CRM must be configured to enable CRMs, using the COM Explorer. The application components that want to use the CRM simply create instances of the CRM Worker class and call methods of its CRM-specific interface to access the resource managed by the CRM.
CRMs are useful for managing private resources that you want to participate in transactions without writing a complete resource dispenser and resource manager. Developers of general-purpose resources will probably want to provide full resource dispensers.
In addition to features that help you use resources more efficiently, COM+ offers enhancements to the security model introduced in MTS. COM+ server applications can choose between role-based security, process access permissions (as used by COM applications today), or no security at all. COM+ library applications can be secured as well, unlike MTS library packages. However, because library applications run within another process, you cannot use process access permissions to secure a library application—you can only use role-based security.
Applications that use process access permissions can take advantage of additional security providers on Windows NT 5 and can use delegate-level impersonation and cloaking to affect the security credentials used during method calls.
In particular, the Kerberos security provider is supported; this provider supports remote mutual authentication between client and server machines. A Secure Sockets Layer (SSL) provider might be supported as well, but it was not supported in Windows NT 5 beta 2.
Delegate-level impersonation is established by the client process and allows a server to impersonate the client and to pass the client's credentials to remote machines. Previously, servers could only impersonate the client to access local resources. Cloaking is used to hide an intermediate server's identity from a destination server. For example, if ClientA calls ServerB, which then calls ServerC on ClientA's behalf, and ServerB has cloaking enabled, ClientA's identity will be used for calls to ServerC.
COM+ role-based security builds on top of the process access permissions security model, just as MTS role-based security is built on top of COM security. For applications using role-based security, COM+ performs security checks for all calls across an application boundary. Thus, if a library application is used by a server application and the library application is enabled for security checks, calls from server application components to library application components will be checked. In addition to application-level, component-level, and interface-level checks, COM+ permits roles to be assigned to individual methods of an interface.
COM+ also provides more extensive information about the security properties in effect for a particular call through the ISecurityCallContext interface. This interface supercedes the ISecurityProperty interface used in MTS. ISecurityCallContext lets you retrieve information about the identities of every caller along a particular chain of calls within an activity. You can also determine the security provider used to authenticate each call, the authentication level used, and the impersonation level used. This information can give you a very accurate picture of the events leading up to a particular method call, which is quite useful for creating audit trails, for example.
To date, COM and MTS have been used primarily for time-dependent applications. As we saw in Chapter 6, a time-dependent application is one in which the caller and callee must coexist at the time a method is called. This is the familiar synchronous method call model used by programming languages and COM. Time-dependent applications are easy to write, but they fail if the callee is unavailable. Many distributed applications do not really need to be time dependent. There might not be any requirement that the callee process calls immediately—as long as the calls are processed eventually. These applications are called time-independent applications.
Message queuing middleware can be used to implement time-independent applications. Communication occurs using one-way messages, which are sent to a queue by the caller for later retrieval by the receiving application. As we saw in Chapter 15, message queuing software such as MSMQ has its own set of API functions to master, with a unique programming model. This makes it difficult for developers to use message queuing in their applications. In COM+, developers can easily take advantage of MSMQ, without using the MSMQ API functions directly, using a subset of the standard COM+ programming model. The COM+ Queued Component system services handle all the details of queuing internally.
Figure 16-1 illustrates how queued components work. When a client application requests an object, the queued components Recorder is created for the object's class. The Recorder collects calls until a client-side transaction is ready to commit and then places one message in an MSMQ queue. This message contains information about all the calls made on the object. If the message is not added to the queue successfully, the transaction will abort.
Figure 16-1. COM+ queued components.
On the server machine, when the application containing the queued component is running, COM+ creates a Queued Component Listener to retrieve messages from the queue. The Listener will pass the message on to a Player object, which translates the message back into COM method calls. These calls are passed on to the component like any other call. As on the client, removing a message from the queue is a transacted activity. If the message cannot be removed from the queue, the application server transaction will abort. If a queue contains a message that causes repeated rollbacks, the message is moved to a dead-letter queue so that the system administrator can examine the message and take corrective action as needed.
To use the Queued Component service, you simply create a COM class as usual. There are a few restrictions on the interfaces the class exposes for time-independent use. Most important, the interface methods can have only [in] parameters. The methods cannot pass information back to the caller using a method parameter or the method return code because the act of calling the method and the act of actually making the method call can happen at completely different times. The caller might not even exist when the actual method call is made.
Developers or administrators configure the class interfaces and their containing applications to use the Queued Component service by using the COM Explorer. The same component can be used either as a queued component or as a regular component with direct method calls. Each interface may be marked as a queueable interface using the COM Explorer. A class that exposes one or more interfaces marked as queueable is considered a queueable class. Likewise, a component containing one or more queueable classes is a queueable component. The containing application must be marked as a queued application in order for the Queued Component service to be invoked. You can also specify whether to automatically start the Listener using the COM Explorer. When a client application is exported, the relevant queue information will be included in the application.
Like the component developer, the client developer uses normal COM programming techniques and tools to access queued components. Objects are created using the moniker-based creation mechanism described earlier. A queue moniker is used to indicate that the client wants to create a queued component, as shown here:
Dim queuedObject as IMyQueuedInterface Set queuedObject = GetObject("queue:/new:MyQueuedComponentProgID")
Once an object has been created, you can make method calls and set properties, just as for any other COM+ object. When you have finished with the object, you simply release it.
The main difference, for developers, between using queued components and regular COM components is in passing information back from the component to the caller. Queued components have a one-way communication path. One technique for dealing with this limitation is to create a second queued notification component that runs on the client application machine. The original component calls back to the client machine through this notification component. Another approach is to have the client application query an application-provided data store maintained by the queued component to obtain information about the results of a method call.
Another issue for application developers is recovering from transactions that fail on the server side after committing on the client. In this case, the client must be notified that an error has occurred and the client must then perform a compensating transaction to recover.
Queued components offer many of the benefits of message-based applications using the standard COM+ programming model. Because queued components are regular COM+ components (with certain restrictions on their interfaces), client applications can choose whether to call components deployed with queuing enabled using queued calls or regular calls.
COM+ Events offer another new model to the application designer: the publish-and-subscribe model. Today in COM and MTS, there is a tight binding between the caller and the callee—that is, the caller must create a new object or be given an interface pointer to an existing object before it can make a call. Both the caller and the callee must exist at the same time with a communication path between them. If a caller wants to broadcast information to multiple callees, it must maintain the list of callees and make a synchronous call to each member of the list. (You might recognize this as the connection point model for events.) However, in many circumstances this tight binding is inappropriate.
In the publish-and-subscribe model, callers (or publishers) publish information via COM method calls, without worrying about who wants the information. The publisher informs the COM+ Event service that it has information to publish. Subscribers let the Event service know that they want to receive new information from the publisher as it becomes available. The Event service keeps track of subscriptions and forwards information from publishers to subscribers. The publisher does not need to know anything about the subscribers, and the subscribers don't need to know anything about the publisher other than the interface used to provide information.
Figure 16-2 shows how the COM+ Event service works. An application that wants to publish events (Publisher) creates and initializes an EventClass object to describe a COM class, known as an event class, that Publisher will use to fire events. The Publisher application does not actually implement the event class; it simply defines the class GUID, ProgID, and event interface and provides a type library. Once the EventClass object is initialized, Publisher tells the EventSystem to store information about the event class. Publisher's events are now available to subscribers.
Figure 16-2. The COM+ Event service architecture.
Components that want to subscribe to events simply implement the event interface exposed by Publisher's event class. They do not need to contain any code to create the subscriptions; this can be done outside the components themselves. The subscription is defined by creating and initializing an EventSubscription object, which defines the relationship between Publisher, event class, and subscriber. Once the EventSubscription object is initialized, the EventSystem object is told to store information about the subscription. The subscription can be persisted so that it remains available until it is removed from EventSystem.
To fire an event, Publisher creates an instance of its event class and calls methods on the event interface. The COM+ Event service will provide an implementation of the event class, based on information provided by Publisher. The method call implementations take care of passing the calls along to the event interface exposed by each subscriber. The COM+ Event service will create subscriber objects, if necessary.
In addition to this basic broadcast of events between a publisher and all subscribers, both publishers and subscribers can restrict how events are delivered. Publishers can write publisher filters, which are used to limit which subscribers receive a particular event. Subscribers can implement a special interface, ISubscriberControl, that is invoked just before an event is fired to the subscriber. The subscriber can use this mechanism to filter out particular events that aren't interesting to the subscriber or to transform the event into a call to a different interface method.
By adding a level of indirection between publishers of information and subscribers, the COM+ Event service offers a powerful way to connect independently developed applications. Since publishing events is no more complicated than making a few COM method calls and subscribers are simply COM components that implement a particular COM interface, it's easy for developers using any programming language to use the COM+ Event service.
Another new feature in COM+ is the In-Memory Database (IMDB). IMDB is exactly what it sounds like: a database that maintains its tables in memory. There are several reasons why having such a database might be useful.
Many applications need to retrieve fairly static data from persistent tables. For example, an application might use a table of valid zip codes and the corresponding city, state, and area code to automatically fill in the city and state fields of a data entry form when the user enters a zip code. A business object might use the table to validate the phone number for a given address. This table will be very large, and retrieving it from a persistent database over and over again will lead to performance problems. By using IMDB as a cache for the persistent database, the table can be loaded once from the persistent database into an IMDB database. Data objects would be configured to retrieve information from the IMDB database rather than the persistent database. Because memory is relatively cheap, this approach can be an inexpensive way to improve the performance of your applications.
While caching is primarily useful for read-only data, it can also be used for read/write scenarios. This technique can be useful when an application's data objects reside on a different machine than the database server. To reduce the network traffic involved in reading and writing the database, an IMDB cache can be configured on the data object server machine. Data objects can read and write to the IMDB. Updated records are propagated back to the persistent store when transactions commit. This approach works particularly well when the database can be partitioned such that only one machine is updating a particular subset of records through the cache.
IMDB also offers a powerful alternative to the Shared Property Manager (SPM) for managing shared transient state. Unlike the SPM, IMDB can be used to share information across server processes. Because IMDB is an OLE DB provider, ADO can be used to access information stored in an IMDB database. Developers are more likely to be familiar with the ADO interfaces than the SPM interfaces, making it easier to implement shared transient state.
Figure 16-3 shows how IMDB works. The COM Explorer is used to configure the IMDB server process to run on a particular machine. This process runs as a Windows NT service. The IMDB server process is responsible for managing IMDB tables and interacting with any underlying persistent databases. The database tables are kept in shared memory; the COM Explorer can be used to configure how much memory is set aside for these tables. IMDB also provides a proxy, which is an OLE DB provider that runs in each client process. The proxy can interact directly with the IMDB tables for read-only scenarios, but it must go through the server process for read/write scenarios or to request locks on the data.
Figure 16-3. Accessing the COM+ IMDB.
In COM+ 1.0, IMDB is limited to a single machine. If information is cached from a persistent database to an IMDB database running on multiple machines, there is no coordination across machines. Likewise, transient state managed by an IMDB database is local to a machine. However, future versions of IMDB are expected to include a distributed cache coordinator and a lock manager, to enable coordination across machines.
IMDB imposes some restrictions on the types of database tables it supports. In particular, the tables must have primary keys, and all changes to the tables must be performed by the IMDB. This means that some database features such as auto-increment fields, time-stamp fields, and triggers cannot be used in an IMDB database. In addition, IMDB does not provide a query processor and does not permit tables to be partially loaded from a persistent store; instead, the entire table must be loaded. The table can be sorted and filtered using ADO Recordset objects.
Despite these restrictions, using IMDB from your components is straightforward. You can use either OLE DB or ADO to access IMDB tables. To do so, you provide a DSN to establish the database connection and then use normal OLE DB or ADO methods. The DSN is configured using the COM Explorer. You also use the COM Explorer to map the IMDB data source to a persistent data source and to define which tables, if any, are loaded from the persistent data source when the IMDB server process starts.
There are a few restrictions on which features of OLE DB or ADO you can use when IMDB is the data provider; these are documented in the Component Services section of the Platform SDK.
The preceding services are primarily developer oriented—to use these services, applications or components must be coded in a particular way. COM+ also offers a new service that does not require any special coding: dynamic load balancing.
As user demand on their systems grows, businesses need to be able to scale their applications to meet demand. A good way to scale is to replicate application servers across multiple machines. This technique also helps improve application availability—if one machine fails, clients can be redirected to a replica. Load balancing can be used to determine which replica each client connects to. The simplest approach to load balancing is static load balancing. With this approach, clients are assigned to a server machine by an administrator and always use that server machine. This method quickly breaks down when there are large numbers of clients who might not be known to the administrator. In addition, it cannot easily adapt to changing server load or server failures.
A better approach is dynamic load balancing. Dynamic load balancing is easier to administer, but it requires additional support from the system infrastructure. With this approach, applications are assigned to server groups and automatically replicated to all machines in the group. Clients are configured to use a load balancing router that will transparently direct client requests to a suitable machine based on dynamic run-time information collected by the load balancing analyzer. In COM+, server groups are called application clusters.
Figure 16-4 illustrates how COM+ load balancing works. First a COM+ server application is defined. Each component in the application that should be load balanced is configured to use load balancing by setting an attribute in its component library. The application is deployed on multiple machines, which are then defined as an application cluster using the COM Explorer. One of the machines in the cluster is selected as the load balancing router. Client machines are configured to create objects on the load balancing router. COM+ intercepts object creation requests on this machine and uses information collected from the other machines in the application cluster to determine where the object should be created. Once the object is created, the client communicates directly with the object; the router is not invoked on every method call.
Figure 16-4. COM+ dynamic load balancing.
The load balancing service runs on all the machines in the application cluster. The router machine runs a load balancing analyzer to collect information from the other machines in the application cluster.
In its first release, COM+ will provide a simple response-time load balancing analyzer; however, the underlying architecture is completely general. In future releases, you might be allowed to add custom analyzers to the system.
The COM+ load balancing service gives administrators a straightforward way to scale applications to support increasing numbers of users without degrading application performance. Since it does not require any coding changes to applications or components, administrators can use this service with any application that has been installed into COM+ using the COM+ administrative tools.