Pooling

This sample demonstrates how to extend Windows Communication Foundation (WCF) to support object pooling. The sample demonstrates how to create an attribute that is syntactically and semantically similar to the ObjectPoolingAttribute attribute functionality of Enterprise Services. Object pooling can provide a dramatic boost to an application's performance. However, it can have the opposite effect if it is not used properly. Object pooling helps reduce the overhead of recreating frequently used objects that require extensive initialization. However, if a call to a method on a pooled object takes a considerable amount of time to complete, object pooling queues additional requests as soon as the maximum pool size is reached. Thus it may fail to serve some object creation requests by throwing a timeout exception.

Note

The setup procedure and build instructions for this sample are located at the end of this topic.

The first step in creating a WCF extension is to decide the extensibility point to use.

In WCF the term dispatcher refers to a run-time component responsible for converting incoming messages into method invocations on the user’s service and for converting return values from that method to an outgoing message. A WCF service creates a dispatcher for each endpoint. A WCF client must use a dispatcher if the contract associated with that client is a duplex contract.

The channel and endpoint dispatchers offer channel-and contract-wide extensibility by exposing various properties that control the behavior of the dispatcher. The DispatchRuntime property also enables you to inspect, modify, or customize the dispatching process. This sample focuses on the InstanceProvider property that points to the object that provides the instances of the service class.

The IInstanceProvider

In WCF, the dispatcher creates instances of the service class using a InstanceProvider, which implements the IInstanceProvider interface. This interface has three methods:

  • GetInstance: When a message arrives the Dispatcher calls the GetInstance method to create an instance of the service class to process the message. The frequency of the calls to this method is determined by the InstanceContextMode property. For example, if the InstanceContextMode property is set to PerCall a new instance of service class is created to process each message that arrives, so GetInstance is called whenever a message arrives.

  • GetInstance: This is identical to the previous method, except this is invoked when there is no Message argument.

  • ReleaseInstance: When a service instance's lifetime has elapsed, the Dispatcher calls the ReleaseInstance method. Just like for the GetInstance method, the frequency of the calls to this method is determined by the InstanceContextMode property.

The Object Pool

A custom IInstanceProvider implementation provides the required object pooling semantics for a service. Therefore, this sample has an ObjectPoolingInstanceProvider type that provides custom implementation of IInstanceProvider for pooling. When the Dispatcher calls the GetInstance method, instead of creating a new instance, the custom implementation looks for an existing object in an in-memory pool. If one is available, it is returned. Otherwise, a new object is created. The implementation for GetInstance is shown in the following sample code.

object IInstanceProvider.GetInstance(InstanceContext instanceContext, Message message)
{
    object obj = null;

    lock (poolLock)
    {
        if (pool.Count > 0)
        {
            obj = pool.Pop();
        }
        else
        {
            obj = CreateNewPoolObject();
        }
        activeObjectsCount++;
    }

    WritePoolMessage(ResourceHelper.GetString("MsgNewObject"));

    idleTimer.Stop();

    return obj;          
}

The custom ReleaseInstance implementation adds the released instance back to the pool and decrements the ActiveObjectsCount value. The Dispatcher can call these methods from different threads, and therefore synchronized access to the class level members in the ObjectPoolingInstanceProvider class is required.

void IInstanceProvider.ReleaseInstance(InstanceContext instanceContext, object instance)
{
    lock (poolLock)
    {
        pool.Push(instance);
        activeObjectsCount--;

        WritePoolMessage(
        ResourceHelper.GetString("MsgObjectPooled"));

        // When the service goes completely idle (no requests 
        // are being processed), the idle timer is started
        if (activeObjectsCount == 0)
            idleTimer.Start();                     
    }
}

The ReleaseInstance method provides a “clean up initialization” feature. Normally the pool maintains a minimum number of objects for the lifetime of the pool. However, there can be periods of excessive usage that require creating additional objects in the pool to reach the maximum limit specified in the configuration. Eventually, when the pool becomes less active, those surplus objects can become an extra overhead. Therefore, when the activeObjectsCount reaches zero, an idle timer is started that triggers and performs a clean-up cycle.

Adding the Behavior

Dispatcher-layer extensions are hooked up using the following behaviors:

  • Service Behaviors. These allow for the customization of the entire service runtime.

  • Endpoint Behaviors. These allow for the customization of service endpoints, specifically a Channel and Endpoint Dispatcher.

  • Contract Behaviors. These allow for the customization of both ClientRuntime and DispatchRuntime classes on the client and the service respectively.

For the purpose of an object pooling extension a service behavior must be created. Service behaviors are created by implementing the IServiceBehavior interface. There are several ways to make the service model aware of the custom behaviors:

  • Using a custom attribute.

  • Imperatively adding it to the service description’s behaviors collection.

  • Extending the configuration file.

This sample uses a custom attribute. When the ServiceHost is constructed it examines the attributes used in the service’s type definition and adds the available behaviors to the service description’s behaviors collection.

The interface IServiceBehavior has three methods in it -- Validate, AddBindingParameters, and ApplyDispatchBehavior. The Validate method is used to ensure that the behavior can be applied to the service. In this sample, the implementation ensures that the service is not configured with Single. The AddBindingParameters method is used to configure the service's bindings. It is not required in this scenario. The ApplyDispatchBehavior is used to configure the service's dispatchers. This method is called by WCF when the ServiceHost is being initialized. The following parameters are passed into this method:

  • Description: This argument provides the service description for the entire service. This can be used to inspect description data about the service’s endpoints, contracts, bindings, and other data.

  • ServiceHostBase: This argument provides the ServiceHostBase that is currently being initialized.

In the custom IServiceBehavior implementation a new instance of ObjectPoolingInstanceProvider is instantiated and assigned to the InstanceProvider property in each DispatchRuntime in the ServiceHostBase.

void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
    // Create an instance of the ObjectPoolInstanceProvider.
    ObjectPoolingInstanceProvider instanceProvider = new
           ObjectPoolingInstanceProvider(description.ServiceType, 
                                                    minPoolSize);

    // Forward the call if we created a ServiceThrottlingBehavior.
    if (this.throttlingBehavior != null)
    {
        ((IServiceBehavior)this.throttlingBehavior).ApplyDispatchBehavior(description, serviceHostBase);
    }

    // In case there was already a ServiceThrottlingBehavior 
    // (this.throttlingBehavior==null), it should have initialized 
    // a single ServiceThrottle on all ChannelDispatchers.  
    // As we loop through the ChannelDispatchers, we verify that 
    // and modify the ServiceThrottle to guard MaxPoolSize.
    ServiceThrottle throttle = null;

    foreach (ChannelDispatcherBase cdb in 
            serviceHostBase.ChannelDispatchers)
    {
        ChannelDispatcher cd = cdb as ChannelDispatcher;
        if (cd != null)
        {
            // Make sure there is exactly one throttle used by all 
            // endpoints. If there were others, we could not enforce 
            // MaxPoolSize.
            if ((this.throttlingBehavior == null) && 
                        (this.maxPoolSize != Int32.MaxValue))
            {
                if (throttle == null)
                {
                    throttle = cd.ServiceThrottle;
                }
                if (cd.ServiceThrottle == null)
                {
                    throw new 
InvalidOperationException(ResourceHelper.GetString("ExNullThrottle"));
                }
                if (throttle != cd.ServiceThrottle)
                {
                    throw new InvalidOperationException(ResourceHelper.GetString("ExDifferentThrottle"));
                }
             }

             foreach (EndpointDispatcher ed in cd.Endpoints)
             {
                 // Assign it to DispatchBehavior in each endpoint.
                 ed.DispatchRuntime.InstanceProvider = 
                                      instanceProvider;
             }
         }
     }

     // Set the MaxConcurrentInstances to limit the number of items 
     // that will ever be requested from the pool.
     if ((throttle != null) && (throttle.MaxConcurrentInstances > 
                                      this.maxPoolSize))
     {
         throttle.MaxConcurrentInstances = this.maxPoolSize;
     }
}

In addition to an IServiceBehavior implementation the ObjectPoolingAttribute class has several members to customize the object pool using the attribute arguments. These members include MaxPoolSize, MinPoolSize, and CreationTimeout, to match the object pooling feature set provided by .NET Enterprise Services.

The object pooling behavior can now be added to a WCF service by annotating the service implementation with the newly-created custom ObjectPooling attribute.

[ObjectPooling(MaxPoolSize=1024, MinPoolSize=10, CreationTimeout=30000)]    
public class PoolService : IPoolService
{
  // …
}

Running the Sample

The sample demonstrates the performance benefits that can be gained by using object pooling in certain scenarios.

The service application implements two services -- WorkService and ObjectPooledWorkService. Both services share the same implementation -- they both require expensive initialization and then expose a DoWork() method that is relatively cheap. The only difference is that the ObjectPooledWorkService has object pooling configured:

[ObjectPooling(MinPoolSize = 0, MaxPoolSize = 5)]
public class ObjectPooledWorkService : IDoWork
{
    public ObjectPooledWorkService()
    {
        Thread.Sleep(5000);
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService instance created.");
    }

    public void DoWork()
    {
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService.GetData() completed.");
    }        
}

When you run the client, it times calling the WorkService 5 times. It then times calling the ObjectPooledWorkService 5 times. The difference in time is then displayed:

Press <ENTER> to start the client.

Calling WorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling WorkService took: 26722 ms.
Calling ObjectPooledWorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling ObjectPooledWorkService took: 5323 ms.
Press <ENTER> to exit.

Note

The first time the client is run both services appear to take about the same amount of time. If you re-run the sample, you can see that the ObjectPooledWorkService returns much quicker because an instance of that object already exists in the pool.

To set up, build, and run the sample

  1. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.

  2. To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  3. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.

Note

If you use Svcutil.exe to regenerate the configuration for this sample, be sure to modify the endpoint name in the client configuration to match the client code.

ms751482.Important(en-us,VS.100).gif Note:
The samples may already be installed on your machine. Check for the following (default) directory before continuing.

<InstallDrive>:\WF_WCF_Samples

If this directory does not exist, go to Windows Communication Foundation (WCF) and Windows Workflow Foundation (WF) Samples for .NET Framework 4 to download all Windows Communication Foundation (WCF) and WF samples. This sample is located in the following directory.

<InstallDrive>:\WF_WCF_Samples\WCF\Extensibility\Instancing\Pooling