Solving Sharepoint ClientContext Socket Exception in C#

How to prevent SocketException 'Address Already in Use' when using Sharepoint CSOM

So, you’ve been writing a Sharepoint client application and it’s running just fine. After several weeks or months, you noticed some errors were showing up. If you see a lot of SocketException error messages like following snippet, then this article is for you.

System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted 127.0.0.1:443
   at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)
   at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Socket s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state, IAsyncResult asyncResult, Exception& exception)

In my previous article about Sharepoint CSOM Socket Exception, I wrote about on how to solve this issue by editing some registry values. I did it and worked beautifully, but only for several days.

So, I did some digging the server to see why on earth this exception still happening. To do that, you can open Command Prompt and use netstat command.

netstat -anob -p tcp > netstat.txt

Open the generated netstat.txt and take a look at it.

In my case I’ve seen a lot of ports being used or in TIME_WAIT state. They are the Sharepoint client application itself, dns.exe, and mostly WindowsAzureGuestAgent.exe.

Now I don’t want to start an argument what cause this and which process start this. I tried to investigate that, but I didn’t find satisfying answer.

But then I got an idea.

I found out about how Sharepoint PnP extends its ClientContext class with ExcuteQueryRetry. This is being used to prevent Sharepoint Online throttling. But with the same idea, it can be used to prevent SocketException as well.

Simplified Scenario

Now before we make the solution, let’s make a simple scenario. You received data from somewhere, for example external REST API, and store it in memory. This series of data length is unknown, so you decided to cache it 100 data at a time and save it to Sharepoint list. Because you don’t know how the series of data will end, this will happened in multiple batches.

To make it simple, let’s create a function to create 100 items in multiple batches. You need to make a new Console project first. Here is an example on how to create 100 new list items in one execution.

static void AddRandomItems(ClientContext ctx, int batch)
{
    for (var i = 0; i < 100; i++)
    {
        // Assume that the web has a list named "Announcements". 
        var announcementsList = ctx.Web.Lists.GetByTitle("Announcements");

        // We are just creating a regular list item.
        var itemCreateInfo = new ListItemCreationInformation();
        var newItem = announcementsList.AddItem(itemCreateInfo);
        newItem["Title"] =
            $"Batch #{batch}, Item #{DateTime.UtcNow.ToString("R")}";
        newItem.Update();
    }
}

Normally, we’re using a ClientContext object to send it to Sharepoint Server and dispose it when it’s done.

// Change it to your Sharepoint list URL
static readonly string ListURL = "http://example.com/list";

static void ExecuteNormally(int batch)
{
    using (var context = new ClientContext(ListURL))
    {
        AddRandomItems(context, batch);
        context.ExecuteQueryRetry();
    }
}

Now let’s create a simulation for sending multiple data to Sharepoint Server in our Main function. We’re going to use 256 loop for starter. If you don’t received an Exception using this loop, try double it or more.

static void Main(string[] args)
{
    // Change to larger loop number if you don't see SocketException
    var loop = 256;

    for (var i = 0; i < loop; i++)
    {
        ExecuteNormally(i);
    }
}

Now let’s try execute it. You probably will get SocketException quickly if you run it in the same machine with your Sharepoint Server.

Solution

What we’re looking to solve this matter is when we received SocketException with SocketError.AddressAlreadyInUse value. We can do that by seeing the InnerException of WebException object.

Here is the raw picture of the code. Normally, when this happened, we need to retry the process again.

try
{
    using (var ctx = clientContext.Clone(clientContext.Url))
    {
        // Put your code HERE

        ctx.ExecuteQueryRetry();
    }
    return;
}
catch (Exception ex)
{
    if (ex is WebException webex
        && webex.InnerException is SocketException sockex
        && sockex?.SocketErrorCode == SocketError.AddressAlreadyInUse)
    {
        // Do something when `AddressAlreadyInUse` caught.
        // For example, retry above process
    }
    else
    {
        // Forward exception if it's not `AddressAlreadyInUse`
        throw;
    }
}

As I mentioned earlier, I got inspiration from Sharepoint PnP on how they prevent throttling by retrying the execution of ExecuteQuery. So I make a helper class that will retry the process (not just ExecuteQuery, but the whole block after CLientContext object created) when we received AddressAlreadyInUse. You can see following code.

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using static Microsoft.SharePoint.Client.ClientContextExtensions;

namespace SPSafeSocketDemo
{
    public static class SafeSocketProcess
    {
        public static void Execute(
            ClientContext clientContext,
            Action<ClientContext> process,
            int retryCount = 10,
            int delay = 500,
            string userAgent = null)
        {
            int retryAttempts = 0;
            int backoffInterval = delay;

            if (retryCount <= 0)
                throw new ArgumentException("Provide a retry count greater than zero.");

            if (delay <= 0)
                throw new ArgumentException("Provide a delay greater than zero.");

            // Do while retry attempt is less than retry count
            while (retryAttempts < retryCount)
            {
                try
                {
                    // Clone client context and execute desired process
                    using (var ctx = clientContext.Clone(clientContext.Url))
                    {
                        process?.Invoke(ctx);
                        ctx.ExecuteQueryRetry(retryCount, delay, userAgent);
                    }
                    return;
                }
                catch (Exception ex)
                {
                    // Check if it's a WSAEADDRINUSE socket error
                    if (ex is WebException webex
                        && webex.InnerException is SocketException sockex
                        && sockex?.SocketErrorCode == SocketError.AddressAlreadyInUse)
                    {
                        // Add delay for retry
                        Task.Delay(backoffInterval).Wait();

                        // Add to retry count and increase delay.
                        retryAttempts++;
                        backoffInterval = backoffInterval * 2;
                    }
                    else
                    {
                        // Forward exception if it's not a WSAEADDRINUSE socket error
                        throw;
                    }
                }
            }

            // Throw an exception when retry count has been attempted.
            throw new MaximumRetryAttemptedException(
                $"Maximum retry attempts {retryCount}, has been attempted.");
        }
    }
}

What this code do is to retry the whole block of Sharepoint server-related process when a SocketException happened. By default it’ll retry 10 times at max. Before every retry, it’ll wait for 500 ms for the first retry, and double it on second retry, and so on. Obviously, you can change its value just like ExecuteQueryRetry.

Let’s Test It

Let’s make a new function based on ExecuteNormally and change it using our helper class SafeSocketProcess.Execute. This is how to use it.

static void ExecuteSafely(int batch)
{
    using (var context = new ClientContext(ListURL))
    {
        SafeSocketProcess.Execute(
            context,
            (ctx) => AddRandomItems(ctx, batch));
    }
}

Change your Main function and use ExecuteSafely instead.

static void Main(string[] args)
{
    // Change to larger loop number if you don't see SocketException
    var loop = 256;

    for (var i = 0; i < loop; i++)
    {
        // ExecuteNormally(i);
        ExecuteSafely(i);
    }
}

Now let’s try running this. You probably won’t receive this SocketException anymore. In my case, I only see 7 retries happened at max.

If you still see the Exception, you can try increase the value of retryCount and delay like following example.

SafeSocketProcess.Execute(
    context,
    (ctx) => AddRandomItems(ctx, batch),
    20,
    1000);

Summary

That’s the simplest method to solve this problem. Hopefully after you implement it, you won’t see any problems anymore.

I know that this method is not highly optimized, you can extend it yourself, for example by adding async and await.

Thanks for reading. You can fork or download the demo project.

References

Subscribe to Junian.net

Get the latest posts delivered right to your inbox.

Delivered by FeedBurner or subscribe via RSS with Feedly!