Object Pooling

A traditional client/server application maps clients directly to server resources in a one-to-one relationship. If there are 10 clients in a system, 10 data objects will be in use. If 1000 clients are attempting to commit data, there will be 1000 data objects and potentially 1000 simultaneous database connections a much less scalable situation.

To counter this problem, COM+ introduces different types of instance management: object pooling and JIT activation. Object pooling is the most significant of the two. It allows a small set of objects to be reused for a large number of clients. Object pooling is conceptually similar to connection pooling: A "pool" of available, instantiated objects is retained permanently. When a client creates the object, the client actually receives a preinitialized copy of the object from the pool. When the client releases the object, it isn't destroyed. Instead, it's returned to the pool, where it's ready to serve another client. You can configure the minimum and maximum size of the pool, ensuring that the server never attempts to create more objects than it can support.

Object pooling provides the following benefits:

  • Reduced object creation cost

    Technically, the cost of creating the object is spread over multiple clients. Although 1000 clients might need to use the object, only 50 objects might need to be created. Even better, these 50 objects will be created only once and then reused.

  • Early object creation

    You can specify a minimum pool size, which ensures that the pool will be populated with the first client request. If an object is particularly expensive to create, you can submit the first request yourself, ensuring that the objects are created and ready for subsequent clients.

  • Resource management

    You also can specify a pool maximum. This doesn't just determine the maximum number of objects in the pool, but also the maximum number of objects that can be created at any one time, whether they are available in the pool or currently in use. If you specify a maximum of 10 and all 10 instances are in use, for instance, the next client request is put on hold until an instance is free. Therefore, you can use a pooled object to wrap limited resources.

If you understand these three benefits, you understand the motivation behind object pooling. The first advantage, reduced object creation cost, allows distributed applications to support high-client, low-throughput systems in which the cost of creating an object is nontrivial. This problem was introduced in Chapter 1. The third benefit, resource management, is equally important. It enables you to limit the use of objects when they require specialized hardware resources or individual licenses. In this way, large client loads can be served slowly yet effectively. Without this technique, the entire system fails when the load is too great.

The Ideal Pooled Component

Object pooling isn't suited for all situations. An ideal candidate for object pooling has the following characteristics:

  • It performs time-consuming work in the constructor.

    By pooling the object, you save time by reducing the number of times the constructor logic must execute.

  • It is stateful.

    Presumably, the time-consuming logic in the constructor initializes the object to a certain state. You want to maintain this state so you don't need to perform time-consuming code to re-create it.

  • It doesn't hold client-specific state.

    A pooled object typically has a long lifetime, during which it is reused by more than one client. You can't maintain client-specific information when the object is released because the client changes.

One exception applies to this list. If you're creating a pooled object solely to gain the benefit of resource management (in other words, you need to limit access to a scarce resource), your object could theoretically still be stateless. In this case, however, it's still likely (and usually more efficient) that the object holds some information about the limited resource in memory.

Creating any object requires basic overhead. This overhead is quite small with COM, however, and even smaller with .NET. You should never use object pooling just to avoid the overhead of creating an ordinary object. As a rule, the types of objects that benefit most from object pooling are those that take a long time to initialize relative to the amount of time required to execute an individual method.

Note

This point is worth repeating: the benefits of object pooling are minimal if the object is inexpensive to create. COM+ uses extra memory and processing time to manage an object pool. A good pooled object performs as much work as possible in its constructor (such as connecting to a file or database and retrieving information).


Pooling and State

One confusing point about object pooling is that it's almost always used in conjunction with stateful objects. There's not much benefit to pooling a stateless object you're better off just re-creating the object for each new client. However, modern programming practices encourage you to use stateless objects. Stateless objects are easier to scale to multiple machines, have minimal overhead, and lend themselves well to .NET Remoting and XML Web services. So why would you ever use a stateful object?

The answer is that pooled objects are never provided directly to clients in a .NET distributed application. Instead, the client contacts a service provider object, which in turn uses the pooled object to perform a specific task. The service provider retrieves the pooled object at the beginning of every method call and releases it at the end, before returning any information to the client. In this way, the client gains all the benefits of stateless objects and your internal system uses its limited resources in the most effective manner possible. Figure 9-7 depicts the arrangement.

Figure 9-7. A design pattern for pooled object use

graphics/f09dp07.jpg

One nice aspect of this design is that it prevents so-called greedy clients clients that refuse to release an object so it can be made available for other requests. You can use JIT activation to prevent this behavior, but doing so adds complexity and overhead. With object pooling, the problem disappears because the client never interacts with the pooled object directly. It can retain a reference to the service provider, but the actual pooled object will be held only during the execution of a service provider method.

The approach shown in Figure 9-7 also neatly sidesteps DCOM questions because the service provider and pooled component are hosted on the same computer (the remote server) and can communicate directly. However, the service provider must be an XML Web service or a remote component.

Pooled Behavior in Windows 2000 vs. Windows XP

Pooled behavior is different in Windows 2000 and in Windows XP. This difference largely results from the fact that Windows XP introduces COM+ 1.5 (which is discussed at the end of this chapter). In Windows 2000, a serviced library component is created in a default application domain and can be shared between all clients machine-wide. In Windows XP, pooled applications are limited to the application domain where they were created. This means that to share a pooled object between clients, you should ensure that all requests are served in the same application domain (possibly the application domain of a component host or the application domain used by the ASP.NET worker processes). If you use server activation, this difference does not appear, and pooled objects are always created in their own application domain.

A Pooled Component Example

This chapter has explained how you should use a pooled component, but it hasn't done much to demonstrate what a pooled component really is. The next example examines the difference between ordinary and pooled objects.

Consider the serviced component shown in Listing 9-3. When it's first created, it opens a connection to a database, retrieves some information, and stores it in the _Data member variable. This information can be retrieved through the Data property.

Listing 9-3 A nonpooled data class
 Public Class NonPooledDataReader     Inherits ServicedComponent     Private ConnectionString = "Data Source=localhost;" + _       "Initial Catalog=Store;Integrated Security=SSPI"     Private _Data As DataSet     ' The constructor opens a database connection, and retrieves a list     ' of products.     Public Sub New()         Dim con As New SqlConnection(ConnectionString)         Dim cmd As New SqlCommand("SELECT * FROM Products", con)         Dim adapter As New SqlDataAdapter(cmd)         Dim ds As New DataSet()         con.Open()         adapter.Fill(ds)         con.Close()        _Data = ds     End Sub     Public ReadOnly Property Data() As DataSet         Get             Return _Data         End Get     End Property End Class 

Let's design a simple client that uses this component. (See Listing 9-4.) It creates an instance of the NonPooledDataReader class each time a button is clicked and displays the retrieved data. Figure 9-8 shows the results.

Listing 9-4 A client for the nonpooled class
 Public Class NonPooledClientTest     Inherits System.Windows.Forms.Form     Private Sub cmdGetData_Click(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles cmdGetData.Click         DataGrid1.DataSource = Nothing         Dim DBReader As New TestComponent.PooledDataReader()         DataGrid1.DataSource = DBReader.Data     End Sub End Class 
Figure 9-8. The simple client

graphics/f09dp08.jpg

So far, this is a fairly predictable segment of code. Every time the button is clicked, the object is reconstructed, the data is requeried, and the DataGrid is updated.

Consider what happens when this object is replaced by the pooled equivalent shown in Listing 9-5. You'll see several highlighted changes. The <ObjectPooling> attribute is added to indicate that this object should be pooled. The CanBePooled method is overridden so it returns True, ensuring the object is placed in the pool when it is released. Finally, an additional variable, named ConstructorFlag, is set to True every time the constructor executes. This ConstructorFlag isn't a detail you would implement in your own pooled objects, but it proves useful in this simple test.

Listing 9-5 A pooled data class
 <ObjectPooling()> _ Public Class PooledDataReader     Inherits ServicedComponent     Private ConnectionString = "Data Source=localhost;" + _       "Initial Catalog=Store;Integrated Security=SSPI"     Private _Data As DataSet     Public ConstructorFlag As Boolean     Public Sub New()         Dim con As New SqlConnection(ConnectionString)         Dim cmd As New SqlCommand("SELECT * FROM Products", con)         Dim adapter As New SqlDataAdapter(cmd)         Dim ds As New DataSet()         con.Open()         adapter.Fill(ds)         con.Close()         _Data = ds         ' Indicate that the constructor has just executed.         ConstructorFlag = True     End Sub     Public ReadOnly Property Data() As DataSet         Get             Return _Data         End Get     End Property     Protected Overrides Function CanBePooled() As Boolean         Return True     End Function End Class 

The client code is also tweaked, which Listing 9-6 shows. Every time it creates the object, it checks the ConstructorFlag to determine whether the constructor executed. It then sets the ConstructorFlag to False and calls Dispose on the object to explicitly release it. Note that to call Dispose, your client application requires a reference to the System.EnterpriseServices.dll assembly because the implementation for this method is defined in the base ServicedComponent class.

Note

If Dispose isn't called, the object won't be returned to the pool until the .NET garbage collector tracks it down, which will adversely affect other clients, particularly if the pool is small enough that you might quickly run out of instances to satisfy ongoing requests. This is another reason that external clients shouldn't have the ability to directly interact with a pooled object. Instead, your service provider should be the only class that uses a pooled component. It can then ensure that the pooled object is properly released at the end of every method call.


Listing 9-6 A client for the pooled class
 Public Class PooledClientTest     Inherits System.Windows.Forms.Form     Private Sub cmdGetData_Click(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles cmdGetData.Click         DataGrid1.DataSource = Nothing         Dim DBReader As New TestComponent.PooledDataReader()         DataGrid1.DataSource = DBReader.Data.Tables(0)         If DBReader.ConstructorFlag Then             lblInfo.Text = "This data was retrieved from the " + _                            "database in the constructor."         Else             lblInfo.Text = "This data was retrieved from memory."         End If         DBReader.ConstructorFlag = False         ' Return the object to the pool.         DBReader.Dispose()     End Sub End Class 

The pooled object will behave in a very interesting way. The first time you click the Get Data button, the ConstructorFlag variable will be True, indicating that the information was retrieved from the database. On subsequent clicks, however, the ConstructorFlag variable will be False because the constructor hasn't executed. Instead, the already instantiated object is provided directly to your client, as indicated by the client form in Figure 9-9. The information is retrieved from memory (the _Data property) and the database won't be touched in fact, you can even take if off line. This behavior is even more powerful when you consider that the pooled object won't just be reused for requests from the same client; it will also be used to serve requests from different clients.

Figure 9-9. The simple client with a pooled object

graphics/f09dp09.jpg

In this case, we're using a Windows Forms client for testing purposes. Remember that when you deploy this solution, you'll want to wrap the calls to the pooled component with a stateless service provider, as shown in Listing 9-7.

Listing 9-7 A service provider that uses a pooled object
 Public Class ServiceProvider     <WebMethod()>_     Public Sub GetData() As DataSet         Dim DBReader As New TestComponent.PooledDataReader()         Return DBReader.Data     End Sub End Class 

In this example, the PooledDataReader provides a form of caching. The data is retrieved once from the data source and reused for multiple requests. You don't need to use a pooled component to get this ability. XML Web services have built-in caching support, as you'll see in Chapter 12, which allows objects to be retained in memory according to set expiration policies. Components exposed through .NET Remoting have no such capability, however, and might benefit more from pooled component designs.

Establishing a Pool

COM+ gives you the power to fine-tune some aspects of connection pooling, including the following:

  • Minimum pool size

    This is the number of objects created with the first request and the number of objects that are maintained and available in the pool at all times. COM+ automatically creates new objects as needed when the pool size dips. Although the pool size can grow above the minimum size, COM+ periodically performs a cleanup and reduces the number of pooled objects to the specified minimum size.

  • Maximum pool size

    If a client requests the object and the number of existing objects is already equal to the maximum pool size, the client is forced to wait.

  • Creation timeout

    If a client is forced to wait for this amount of time, in milliseconds, an exception is thrown.

Note

Remember that the minimum pool size determines the number of available objects that are maintained in the pool. In other words, if the minimum size is 10, COM+ reserves 10 free objects above and beyond those that are in use. The maximum pool size specifies the maximum number of objects that can be created at any one time, irrespective of how many objects are actually available.


Here's an example attribute that configures a minimum pool size of 5 and a maximum size of 20:

 <ObjectPooling(5, 20)> _ 

You can use event logging to verify that the pool you expect is actually being created. For example, you can add the following code to the constructor of the PooledDataReader:

 ' Define a log message with the unique hash code for this object. Dim LogMessage As String = "Pooled " & Me.GetHashCode() ' Write the message. EventLog.WriteEntry("TestComponent", LogMessage) 

The first time you run the component, you'll see several entries added to the event log: one entry for each object that was created. The total number of entries should correspond to the minimum pool size plus one. The additional object is used to satisfy the current client.

Consider Figure 9-10, for example, which shows six entries. The extra object was created so COM+ could maintain five available objects. After the client releases the extra object, it will return to the pool. Eventually, COM+ will notice the unneeded extra object and remove it.

Figure 9-10. Event log entries for a pooled object

graphics/f09dp10.jpg

Keep in mind that the earlier example in Listing 9-6 might not work as expected if you use a minimum pool size because you might not receive the same object that you release. If you receive another object from the pool, the ConstructorFlag won't have been set to False.

The maximum pool size always takes precedence over the minimum pool size setting. Consider a case where you have a minimum pool size of 10 and 5 connections are currently in use. In this case, 10 additional connections will be created and added to the pool, provided the maximum pool size allows it. If the maximum pool size is less than 15, the number of new connections is constrained. If the maximum pool size is 10, however, there will be only 5 additional objects in the pool rather than the requested minimum of 10.

There are no definitive rules for optimum pool size settings. These settings depend on your system's processing capabilities and on the nature of the resource. (If you're wrapping a limited resource such as a license, for example, you should make the maximum pool size equal to the number of installed concurrent licenses.) Note that if you specify a large minimum pool size, the first request might take a long time to return.

Activation, Deactivation, and Conditional Pooling

Every pooled component can add additional object pooling logic by overriding three methods from the ServicedComponent class: Activate, Deactivate, and CanBePooled.

Deactivate is called by the .NET infrastructure every time a component is released and returned to the pool. It enables you to ensure that the object's in-memory data is in a reasonable state. Activate is called when the object is resurrected, just before it is presented to the client.

COM+ also enables you to programmatically determine whether an object should be pooled by using the CanBePooled method. If you have determined that the object has expired data or is in an inconsistent state, however, you can return False in this method. The object is destroyed when the client calls Dispose. Listing 9-8 shows an example that extends the caching approach by returning False if the object was created more than one hour ago.

Listing 9-8 Customizing the CanBePooled method
 <ObjectPooling()> _ Public Class PooledDataReader     Inherits ServicedComponent     Private ConnectionString = "Data Source=localhost;" + _       "Initial Catalog=Store;Integrated Security=SSPI"     Private _Data As DataSet     Private CreateTime As DateTime     Public Sub New()         Dim con As New SqlConnection(ConnectionString)         Dim cmd As New SqlCommand("SELECT * FROM Products", con)         Dim adapter As New SqlDataAdapter(cmd)         Dim ds As New DataSet()         con.Open()         adapter.Fill(ds)         con.Close() 
        _Data = ds         CreateTime = DateTime.Now     End Sub     Public ReadOnly Property Data() As DataSet         Get             Return _Data         End Get     End Property     Protected Overrides Function CanBePooled() As Boolean         If CreateTime.AddHours(1) > DateTime.Now             Return True         Else             Return False         End If     End Function End Class 

Pooling and Data Providers

Technically, the PooledDataReader class that we examined earlier encapsulates a block of data. However, it's more common to create a pooled object that represents a handle to a data source. Consider the somewhat artificial example shown in Listing 9-9, which holds an open handle to a text file. The assumption here is that with multiple clients, the overhead of initializing this connection is significant compared to the actual file reading operation. (This probably isn't the case with a file, but it is likely with a database.)

Listing 9-9 Using pooling with a data provider
 Public Class PooledFileReader     Inherits ServicedComponent     Private fs As FileStream     Private r As StreamReader     Public Sub New()         fs = New FileStream("C:\data.txt", FileMode.Open)         r = New StreamReader(fs)     End Sub 
     Public Function GetData() As String         fs.Position = 0         Return r.ReadToEnd     End Function     Protected Overrides Function CanBePooled() As Boolean         Return True     End Function End Class 

When implementing a data provider, you commonly need to restrict access. For example, consider the result of using this code in a client:

 Dim r1 As New PooledTextReader() Dim r2 As New PooledTextReader() 

In the Windows operating system, only one process at a time can open a text file. In this case, the second line generates an exception resulting from a file access error. You can defend against this error by implementing a pooling restriction:

 <ObjectPooling(1, 1)> _ Public Class PooledFileReader 

Because both the minimum and maximum pool size are set to 1, there will only ever be one instance of the PooledTextReader object. Technically, it's a singleton component (although it's different from a .NET Remoting singleton in that only one client at a time can use it). Now, when another client attempts to create the object, a COM+ timeout error is eventually generated rather than a file access error.

This approach doesn't appear to be much of an improvement either way, the second line causes an error. However, consider what happens in a more sophisticated case in which the data-provider object corresponds to a licensed resource. If you don't use pooling, when the number of clients rises above the number of licenses, every client starts to experience license violation errors as it attempts to invoke methods. Therefore, the entire system is compromised by the heavy client load. On the other hand, if you use object pooling to restrict the maximum number of objects to the number of available licenses, additional clients are prevented from connecting. Some users (the additional clients) are affected, but those who are currently using the system can carry on, finish their work, and eventually release their objects back to the pool. This scenario also applies when a pooled object is protecting a resource that taxes the server hardware or memory. If too many clients are allowed to connect, the entire system can fail. Resource management entails limiting the maximum client load to guarantee a certain baseline level of connectivity.

Object Pooling vs. Connection Pooling

Most ADO.NET providers include support for connection pooling. Connection pooling is conceptually similar to object pooling. As with object pooling, its goal is to set usage maximums and avoid the overhead of repeatedly creating connections. With connection pooling, the connection is released to the pool when the Close method is called and is acquired when the Open method is invoked. With object pooling, the process is similar: the object is released using a Dispose method and is acquired automatically when it's instantiated.

You can look at connection pooling as a special case of object pooling. In many distributed systems, however, connection pooling provides all the object pooling you need because the key server resource being exposed to remote clients is the database. This is certainly true of the service provider example introduced in Chapter 2.

In the past, connection pooling wasn't sufficient because many forms of connection pooling didn't enforce minimum and maximum pool sizes. They might have reduced the overhead required to acquire a connection, but they failed miserably at restricting the load when too many clients connected at once. OLE DB resource pooling still suffers from this problem. For that reason, developers were sometimes encouraged to disable this OLE DB pooling and use COM+ object pooling in its place. If you're using an OLE DB resource with a large client load, you might still choose to do so (or develop your own custom pooling strategy, as briefly outlined in Chapter 7).

However, both the SQL Server provider and the Oracle .NET provider use their own form of connection pooling that is conceptually similar to COM+ object pooling. That means pool maximums and minimums are strictly enforced. Chapter 12 describes how connection pooling can be configured through connection string settings and develops a sample application to test its success with an XML Web service. With either the SQL Server or the Oracle .NET provider, you have the best that COM+ object pooling can offer, completely for free. The pooling mechanism is also optimized for speed and handily outperforms OLE DB resource pooling.



Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net