Although you know how to create and consume a serviced component, it's of little use unless you use one of the automatic services provided by COM+. The following sections provide an overview of COM+ services. Object Pooling The object pooling service enables you to increase scalability and performance of an application by minimizing the time and resources required in creating objects repeatedly. It does this by maintaining a pool of already created objects and then efficiently reusing them repeatedly. You can configure a class to use the object pooling service by applying the ObjectPooling attribute to the class, as shown in the following example: [ObjectPooling(Enabled=true, MinPoolSize=2, MaxPoolSize=4)] public class NorthwindSC : ServicedComponent, INorthwind { ... } Table 8.4 lists various properties of the ObjectPooling attribute that you can use to configure the way object pooling works for the class. Table 8.4. Properties of the ObjectPooling AttributeProperty | Description |
---|
CreationTimeout | Specifies a length of time (in milliseconds) to wait for an object to become available in the pool before throwing an exception. | Enabled | Specifies whether object pooling is enabled for a component. The default value for this property is true. | MaxPoolSize | Specifies the maximum number of pooled objects that should be created for a component. | MinPoolSize | Specifies the minimum number of objects that should be available to a component at all times. | In addition to using the ObjectPooling attribute, an object-pooled class also overrides the CanBePooled() method of the ServicedComponent class. The overridden version of this method should return either true or false. An object is pooled only if the CanBePooled() method returns true. At a conceptual level, you can envision that COM+ places a pooling manager between the client and the server, as shown in Figure 8.2. Figure 8.2. The pooling manager intercepts any requests for object creation and provides the object pooling service. The pooling manager follows this set of rules for creating objects: When the COM+ application is started, a MinPoolSize number of objects is created and thereafter maintained in the pool at all times when the application is running. Each time the pooling manager receives a request to create an object, the pooling manager checks whether the object is available in the pool. If the object is available, the pooling manager provides an already created object from the pool. If no objects are currently available in the pool, the pooling manager checks to see whether the number of objects currently in the pool has reached the MaxPoolSize. If it has not, the pooling manager creates new objects to fulfill the request. The pooling manager tends to create as many objects as needed to keep the number of available objects at the level of MinPoolSize while not exceeding the MaxPoolSize. If no object is available and no new object can be created because of the size restriction of the pool, the client requests are queued to receive the first available object from the pool. If an object cannot be made available within the time specified in the CreationTimeOut property, an exception is thrown. When the client finishes with an object, it invokes a Dispose() method on the object. The pooling manager intercepts this request and calls the CanBePooled() method on the object to check whether the object is interested in being pooled. If the method returns true, the object is stored in the object pool. On the other hand, if the CanBePooled() method returns false, the object is destroyed. The pooling manager ensures that an optimum number of objects is always available in the object pool. If the number of available objects in the pool drops below the specified minimum, new objects are created to meet any outstanding object requests and refill the pool. If the number of available objects in the pool is greater than the minimum number, those surplus objects are destroyed during a clean-up cycle. | Using the object pooling service with every application might not be a good idea. Although object pooling has benefits, it also has its own share of overheads. You should use object pooling in applications where the benefits of object pooling exceed the overheads. Some of the scenarios suitable for object pooling are as follows: When the costs of creating an object are relatively high When usage costs are relatively low When an object will be reused often When you want to limit the number of object instances
Some scenarios in which object pooling might not be useful are as follows: When an object is inexpensive to create When the object does not maintain any server-specific state When the object must be activated in the caller's context When you do not want to restrict the number of object instances
|
Just-in-Time Activation When you are using object pooling, you can approach object creation from the client in two ways: In the first approach, a client creates an object and holds on to it until the client no longer needs it. The advantage of this approach is that it's faster because the client does not need to create objects repeatedly. The disadvantage is that this approach can be expensive in terms of server resources in a large-scale application. In the second approach, a client can create, use, and release an object. The next time it needs the object, it creates it again. The advantage to this technique is that it conserves server resources. The disadvantage is that as your application scales up, your performance slows down. If the object is on a remote computer, each time an object is created, there must be a network round-trip, which negatively affects performance. The just-in-time activation service of COM+ provides a server-side solution that includes advantages of both of the preceding approaches while avoiding the disadvantages of each. To use this service in a class, all you need to do is mark the class with the JustInTimeActivation attribute set to true. When the client requests a just-in-time activated object from the server, COM+ intercepts the request, creates a proxy, and returns the proxy to the client. The client maintains a long-lived reference to the proxy, thinking that it is a reference to the actual object. This way the client does not spend time repeatedly creating the object. The server also saves resources because it can delay creating the object until the client invokes a method on it. When the client invokes a method on the object (using the proxy), COM+ actually creates the object, calls the method, returns the results, and then destroys the object. Because the objects are short-lived, the server resources are consumed for only a small period and the server is readily available to serve other waiting clients. COM+ maintains a "done bit" to specify when the object will be deactivated. The interception mechanism checks the done bit after each method call finishes. If the value of the done bit is true, the object is deactivated; otherwise, the object continues to exist. The default value of the done bit is false. Nevertheless, you can programmatically set the value of the done bit by using any of the following techniques: The ContextUtil.SetComplete() or ContextUtil.SetAbort() Methods Usually these methods are used to vote for the success or failure of a transaction, but they also set the done bit to true. The ContextUtil.DeactivateOnReturn Property When you set this property to true in a method, the property sets the done bit to true. The AutoComplete Attribute When you always want to deactivate an object when the method call returns, you can apply this attribute to the method definition. This attribute automatically calls ContextUtil.SetComplete() if the method completes successfully. If the method throws an exception, the ContextUtil.SetAbort() method is invoked. In both cases, the done bit is automatically set to true. The JIT activation and object pooling services complement each other's features. When these services are used in combination, they can maximize the throughput for an application by providing the following benefits: JIT activation enables clients to hold long-lived references on the server object (through a proxy) without consuming server resources. JIT activation enables the server objects to be destroyed as soon as their work is over, to minimize the resource consumption on the server. Object pooling caches the already created objects and saves time by activating and deactivating the objects from the pool instead of re-creating them from scratch. Design Considerations for Using Just-in-Time Activation When using JIT activation in your programs, you need to consider the following points: An object's lifetime is controlled by the server instead of the client. Therefore, you do not need to call the Dispose() method on the server object from the client. Actually, if you do so, the object will be re-created on the server, just to be disposed of. The server does not automatically deactivate an object. You need to set the done bit to true for COM+ to destroy an object after the current method call has completed. You can use any of the techniques mentioned in the previous section to set the done bit to true. You can also configure a method administratively to control this behavior. The objects are created and destroyed after each method call. Therefore, you should consider the server object as stateless. JIT activation is not suitable for objects that need to maintain state across method calls. Automatic Transaction Processing A transaction is a series of operations performed as a single unit. A transaction is successful only when all the operations in that transaction succeed. The COM+ transaction-processing mechanism provides automatic transaction services you can use in your program without writing any additional transaction processing code. COM+ can handle transactions ranging from a single transactional resource (such as a SQL Server database) to that involving multiple heterogeneous resources (such as a SQL Server database, Oracle database, and Microsoft Messaging Queue). You can enable a .NET Framework class to use the COM+ automatic transaction processing by applying the Transaction attribute to the class. The Transaction attribute takes a value from the TransactionOption enumeration to specify how a component participates in a transaction. The values of the TransactionOption enumeration are listed in Table 8.5. Table 8.5. Members of the TransactionOption EnumerationMember | Description |
---|
Disabled | Specifies that the component's capability to participate with COM+ in managing transactions has been disabled. This setting is used for compatibility reasons only. | NotSupported | Specifies that the component will never participate in transactions. | Required | Specifies that the component uses transactional resources such as databases and will participate in a transaction if one already exists; otherwise, a new transaction must be created. | RequiresNew | Specifies that the component requires a new transaction to be created even if a transaction already exists. | Supported | Specifies that the component will participate in a transaction if one already exists. This setting is mostly used by components that do not themselves use any transactional resources. | | To preserve the consistency of a transaction, a component must not carry state from one transaction to another. To enforce statelessness for all transactional components, COM+ uses JIT activation. JIT activation forces an object to deactivate and lose state before the object can be activated in another transaction. For a class, if you apply the Transaction attribute and set its value to TransactionOption.Supported, TransactionOption.Required, or TransactionOption.RequiresNew, COM+ automatically sets the JustInTimeActivation attribute to true. |
Each component that participates in a transaction has its own context. The context stores various flags that specify an object's state. Two such flags are the done bit and the consistent bit. In addition to objects, the transaction itself has a context. The context of a transaction maintains an abort bit. The purpose of the abort bit is to determine whether the transaction as a whole failed or succeeded. These bits and their influence on the outcome of a transaction are summarized in Table 8.6. Table 8.6. The Abort, Done, and Consistent Bits and Their Effect on the Transaction OutcomeBit | Scope | Description | Effect on the Transaction Outcome |
---|
abort | Entire transaction | This bit is also called the doomed bit. COM+ sets this bit to false when creating a new transaction. In a transaction lifetime, if this bit is set to true, it cannot be changed back. | If the abort bit is set to true, the transaction is aborted. | consistent | Each context | This bit is also called the happy bit. COM+ sets this bit to true when creating an object. A programmer can choose to set this bit to true or false, depending on the program logic, to indicate that the object is either consistent or inconsistent. | If the consistent bit in any context is set to false, the transaction is aborted. If the consistent bit in all the contexts is set to true, the transaction is committed. | done | Each context | Each COM+ object that participates in a transaction must also support just-in-time activation and, therefore, must maintain a done bit. When a method call begins, the done bit is set to false. When a method call finishes, COM+ checks the status of the done bit. If the bit is true, the active object is deactivated. | When exiting a method, if the done bit is set to true and the consistent bit is set to false, the abort bit is set to true. | The .NET enterprise services library provides the ContextUtil class to work with an object's context. Table 8.7 shows the methods of the ContextUtil class that influence an object's done bit and its consistent bit. Table 8.7. How the Methods of the ContextUtil Class Affect the Consistent Bit and the Done BitMethod | Effect on Consistent Bit | Effect on Done Bit |
---|
DisableCommit() | false | false | EnableCommit() | true | false | SetAbort() | false | true | SetComplete() | true | true | Queued Components From the perspective of a client, a queued component is a serviced component that can be invoked and executed asynchronously. The queued components are based on the Microsoft Message Queuing (MSMQ) technology, which is a part of the Windows operating system. Communication between a client and a queued component involves four basic components between the client and server, as shown in Figure 8.3. Figure 8.3. With queued components, messages are recorded in a message queue for later retrieval. Here is a list of the pieces involved: Recorder The Recorder kicks in when a client makes a call to the queued component. The recorder records the call, packages it as a message, and stores the message in the message queue. Queue The queue is a repository of messages. Each COM+ application has a set of queues assigned to it. There is one primary queue, five retry queues, and one dead-letter queue. When the message arrives from the recorder, the message waits in the primary queue to be picked up by the queued component. If there is an error in processing, the message is sent to the first retry queue. If the message processing fails in the first retry, the message is moved to the second retry queue, and so on. The retry queues differ from each other by the frequency with which they retry a message. The first retry queue retries messages most frequently, whereas the fifth one is the slowest. If there is an error in processing the message in the fifth queue, the message is finally moved to a dead-letter queue, where no further retries are made. Occasionally, you might want to check the dead-letter queue and custom-process any failed messages. Listener The listener's role is to poll the queue for incoming messages and, when there is one, pass the message to the player. Player The player unpacks a message and calls the invoke methods that were recorded by the client on the queued component. To configure a component to work as a queued component, you need to apply the following two attributes: ApplicationQueuing attribute You apply this attribute at the assembly level to enable queuing support for an application. If a client will call the queued component, the QueueListenerEnabled property must be set to true. [assembly: ApplicationQueuing(Enabled=true, QueueListenerEnabled=true)] InterfaceQueueing attribute You apply this attribute at the component level to specify the interface through which COM+ allows the calls on the component to be recorded and stored in the queue. For example: [InterfaceQueuing(Enabled=true, Interface="IOrdering")] The execution lifetime of a queued component and the client application might be different. Therefore, when creating a queued component, you must follow these guidelines: Methods should not return any value or reference parameters. All calls made by the client should be self-sufficient. The queued component has no way to generate a callback to the client program if more information is needed. Methods should not throw any application-specific exceptions because the client might not be available to respond to the exceptions. One of the ways you create an object in the client program is by using the new operator. However, you don't want to use that when working with queue. With a queued component, your objective is not to create an instance of an object; instead, you need a way in which you can record a message for the client and store that in a queue so that the server object can read that message when possible. Recording a message for a queued component is generally a three-step process: Call the Marshal.BindToMoniker() method and pass it a moniker string that corresponds to the interface of the queued component. The moniker string is formed by preceding the full type name (qualified with namespace) with the string "queue:/new:". For example: Marshal.BindToMoniker(@"queue:/new:NorthwindQC.Ordering"); The Marshal.BindToMoniker() method returns a reference to the interface identified by the given moniker string. Use the interface reference obtained in step 1 to execute methods on the queued component. These methods are not executed immediately; instead, they are recorded and placed in the message queue. When you've finished calling methods, call the Marshal.ReleaseComObject() method to release the reference to the interface reference obtained in step 1.
|