A Sample for WCF Client Proxy Pooling

Introduction

Ideally we should not need to pool WCF client proxies as I mentioned in my previous blog entry. From some customer feedback, however, I got to know that reusing proxies is not ideal because:

· There may be some unknown contention cost when one proxy is used by multiple threads.

· There is some concern about associating contextual data to a proxy when the proxy is reused at the same time by multiple threads.

So people would generally think about implementing a proxy pool in their systems. As pointed out in my previous blog entry, you need to be aware of the following when you want to implement a proxy pool:

· You need to implement the right synchronization logic for managing the proxies.

· You need to make sure the proxies are used equally. Sometimes, you may want to implement a round-robin pattern for the proxies.

· You need to handle exceptions and retries for the pool.

· The pool size needs to be limited and configurable.

· You may need to be able to create proxies even if when no proxy is available from the pool.

Here are some common ways to implement a pool in my mind:

· Load-balancing model: Whenever you want to send a request, you would pick a proxy from the pool (in a round-robin pattern) and use it to handle the request. The proxy can be used by multiple threads concurrently.

· Take-return-create model: You take the proxy from the pool so that other threads cannot get the same one. After using it, you return it back to the pool. When the pool is empty, you would create a new proxy. When the pool is full when you return it back, you just discard the proxy.

· Take-return-wait model: Similar as above. The difference is that, when the total number of proxies being used reaches the size of the pool and all proxies are being used (i.e., the pool is empty), the thread would wait until a proxy is returned back to the pool.

I will show a sample to implement the second approach “take-return-create model”. It is easy to implement and it solves performance issues nicely. As what I have written in my other blog entries, the sample code and the ideas in this blog imply no warranties and confer no rights and they are provided AS-IS.

The Pool<T> Class

The generic Pool<T> class that I used in the sample implements the simple Take-Return logic. It is actually the one that is taken from the internal calls System.ServiceModel.Pool<T>. You can disassembly the source from System.ServiceModel.dll with the .NET Reflector. Here is the source:

class Pool<T> where T : class

{

    T[] items;

    int count;

    public Pool(int maxCount)

    {

        items = new T[maxCount];

    }

    public int Count

    {

        get { return count; }

    }

    public T Take()

    {

        if (count > 0)

        {

            T item = items[--count];

            items[count] = null;

            return item;

        }

        else

        {

            return null;

        }

    }

    public bool Return(T item)

    {

        if (count < items.Length)

        {

            items[count++] = item;

            return true;

        }

        else

        {

            return false;

        }

    }

    public void Clear()

    {

        for (int i = 0; i < count; i++)

            items[i] = null;

        count = 0;

    }

}

The logic is very simple. It uses an array-based simple stack to manage the items. Note that the class does not provide thread-safe logic. So we have to add that logic when we use it.

The Pooling Logic

Once we have the pool, we can implement the pooling logic. Here I follow the best practice of closing a proxy as mentioned in the MSDN article https://msdn2.microsoft.com/en-us/library/aa355056.aspx. Here is the code:

void CallWithPool(string message)

{

    MyHelloServiceProxy proxy = null;

    // Trying to take the proxy from the pool

    lock (pool)

    {

        proxy = pool.Take();

    }

    // Remove bad proxies (Faulted etc). Note that checking the proxy state has

    // some performance cost here as it accesses the Transparent proxy of the

    // channel.

    if (proxy != null && proxy.State != CommunicationState.Opened)

    {

        proxy.Abort();

        proxy = null;

    }

    // Create the proxy if not found from the pool.

    if (proxy == null)

    {

        Console.WriteLine("Creating new proxy.");

        proxy = CreateProxy();

    }

    try

    {

        // Call the service with the proxy

        proxy.Hello(message);

    }

    catch (Exception)

    {

        // You need to better handle the exception in real production

        // environments.

        proxy.Abort();

        throw;

    }

    // Trying to put the proxy back to the pool

    bool shouldClose = false;

    lock (pool)

    {

        shouldClose = !pool.Return(proxy);

    }

    // Close the proxy if it's not put back into pool

    if (shouldClose)

    {

        try

        {

            proxy.Close();

        }

        catch (Exception)

        {

            proxy.Abort();

   throw;

        }

    }

}

Comparisons

In the sample, I also implemented the other two approaches for comparison purpose:

· Per call: Creating/disposing proxies per call

· Singleton: Using singleton proxies

Data shows that the performance between pooling and singleton is quite close. When BasicHttpBinding with default settings is used, the “per call” approach has comparable performance as the other two. However, when WSHttpBinding is used, you would see much slower results when “per call” model because of the high cost in re-constructing security sessions when each proxy is created.

Configuration Setting Helper

Now that I have provided all the details, why don’t I be a good person to the end? One thing that we have not mentioned is about configuring the pool size. In the sample, I also provided a simple configuration section <channelPoolSettings> that is implemented by the type “ChannelPoolSettingsSection” . In this way, you can easily change the setting from the configuration without touching the code. This can be easily converted to ASP.NET web.config setting. To make the configuration work, you need to register the section handler as following:

<configSections>

  <section name="channelPoolSettings" type="WCF.Performance.Samples.ChannelPoolSettingsSection, TestChannelPoolClient" />

</configSections>

Here is the sample configuration:

<configuration>

  <channelPoolSettings poolSize="8"/>

</configuration>

You can find the sample code attached.

 

TestChannelPool.zip