Fun with Sockets

Actually, not fun at all. This problem has been driving me nuts for hours.

There are some things I like about .net socket programming, but one thing that always causes horrible problems is shutting sockets down when I’ve finished with them. One way or another, I always end up having to trap some exceptions.

As an example, here’s a fragment from some code I wrote several years ago to receive data on a UDP socket:

    _socket.BeginReceive(new AsyncCallback(dataReceived, null);
...
private void DataReceived(IAsyncResult result) {
    try {
        IPEndPoint ep = null;
        byte[] data = _socket.EndReceive(result, ref ep);
        doStuff(data);
    } catch (ObjectDisposedException) {
        return;
    } catch (ThreadAbortException) {
        return;
    }
}

While the socket is waiting to receive something the program might close it and shut down the thread that it was calling BeginReceive on. Hence it has to trap several possible exceptions. In practice, particularly with TCP sockets, there are more possible exceptions that can occur including, for example, things like Stream IO errors if it happened to be reading when the socket was closed.

These days asynchronous code is all supposed to be much cleaner and easier, because we have things like Tasks, async and await to manage it all. When I started writing some UDP code recently I thought I would try to use it. At the core of receiving there is, of course, a ReceiveAsync call on the UdpClient object. Here’s an immediate problem. How do I tell it that I want to cancel the call? Many Async operations take a cancellation token, and setting that will end the call, but ReceiveAsync doesn’t.

Some hunting around MSDN came up with this, an extension method that allows operations that don’t take a cancellation token to act as though they do:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken) {
    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        if (task != await Task.WhenAny(task, tcs.Task))
            throw new OperationCanceledException(cancellationToken);
   return await task;
}

This lets me create a version of ReceiveAsync that does take a cancellation token.

public static async Task<UdpReceiveResult?> ReceiveAsync
        this UdpClient client,
        CancellationToken cancel) {
    var read = client.ReceiveAsync();
    try {
        return await read.WithCancellation(cancel);
    } catch (OperationCanceledException) { }
    return null;
}

I was almost surprised to find that this worked straight off. Within my server’s constructor I have

_server = new UdpClient(port);
_cancel = new CancellationTokenSource();
_runTask = Task.Run(() => runServer(_cancel.Token), _cancel.Token);

The server thread runs

private async Task runServer(CancellationToken cancel) {
    while (!cancel.IsCancellationRequested) {
        var msg = await _server.ReceiveAsync(cancel);
        if (!cancel.IsCancellationRequested && msg != null) {
            doStuff(msg);
        }
    }
}

and the dispose contains

_cancel.Cancel();
_runTask.Wait();
_server.Close();
_cancel.Dispose();

which all seems quite straightforward.

I’ve been using it in Windows and in Android Xamarin code for a while and it all seemed to be working fine, until I added Xamarin.Insights into the Android app. Insights is an error logging system that traps all untrapped exceptions and sends them to a Xamarin run website, where the developer can look at the exception details. (I am considering writing a PCL version of my error logging system, which should then work on Android, iOS etc., but it would get complicated because there’s a lot missing from PCL, such as File Handling… and Xamarin Insights is very easy to set up).

As soon as I started trying to use this UDP code with Xamarin Insights set up, I started seeing exceptions arriving in Insights. (Previously, these exceptions had probably been getting logged to the Xamarin IDE’s Output window, but I hadn’t spotted them because of the incredible amount of noise that gets sent to it. I didn’t look because there didn’t seem to be any problem). Unfortunately, the exceptions didn’t seem to be getting raised in my code.

System.ObjectDisposedException Cannot access a disposed object. Object name: 'System.Net.Sockets.UdpClient'.
System.Net.Sockets.UdpClient.CheckDisposed()UdpClient.cs:601
System.Net.Sockets.UdpClient.EndReceive(IAsyncResult asyncResult, ref IPEndPoint remoteEP)UdpClient.cs:475
System.Net.Sockets.UdpClient.<ReceiveAsync>m__0(IAsyncResult r)UdpClient.cs:610
System.Func<IAsyncResult, UdpReceiveResult>.invoke_TResult_T(IAsyncResult)(wrapper delegate-invoke)
System.Threading.Tasks.TaskFactory<TResult>.FromAsyncCoreLogic(IAsyncResult iar, Func<_,_> endFunction, Action<_> endAction, Task<_> promise, bool requiresSynchronization)FutureFactory.cs:552

What that is showing is that somewhere within ReceiveAsync it does an EndReceive call, like the one at the top of this blog entry, and that is raising the ObjectDisposedException that I had to trap before. But I can’t trap this, because there doesn’t seem to be any of my code on the stack.

After a lot (and I mean a lot) of hunting around and trying out ideas I eventually discovered what was happening. The Exception is being raised within the Finalizer for a Task object.

It seems the Exception is getting trapped by Insights because it subscribes to TaskScheduler.UnobservedTaskException. It gets handled there because nothing else is handling it. Normally, when Wait is called on a Task any exceptions get trapped there. But what if the Wait never completes?

Remember this fragment?

try {
        return await read.WithCancellation(cancel);
    } catch (OperationCanceledException) { }

The await never completes because WithCancellation is throwing its OperationCanceledException. When the other exception occurs on the Task in its Finalizer, there’s nothing to trap it so it goes to the UnobservedTaskException handler.

I’ve worked out a solution. First I’ve created another Task extension

public static void GrabExceptions(this Task task) {
    task.ContinueWith(t => {
            var aggException = t.Exception.Flatten();
            System.Diagnostics.Debug.WriteLine("Grabbed Exceptions:");
            foreach(var exception in aggException.InnerExceptions)
                System.Diagnostics.Debug.WriteLine("Grabbed Exception: " + exception.Message);
        }, 
        TaskContinuationOptions.OnlyOnFaulted);
}

This runs if the Task throws an exception after it has been run, and just logs it to the Debug window (I should probably pass it a collection of Exception types to ignore, in this case just ObjectDisposedException). The task it needs to run on is the one passed to WithCancellation, which now becomes:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken) {
    task.GrabExceptions();
    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        if (task != await Task.WhenAny(task, tcs.Task))
            throw new OperationCanceledException(cancellationToken);
    return await task;
}

Job done. Just one question left, really. Why did none of my Unit Tests turn up this problem? The answer is, I ran them in .net 4.5 and since releasing 4.0 Microsoft realised that this was causing problems and changed the way the Finalizers handle exceptions that occur like this on Tasks – essentially ignoring them, as far as I can see. The Mono code I’m using within Xamarin doesn’t.

The code is part of my general purpose socket library, Babbacombe.SockLib, in the DiscoverServer, and will be updated soon (if not already).

2 thoughts on “Fun with Sockets”

  1. Wow – I was running into the exact same issue! I knew the Task’s finalizer was causing it but I wasn’t sure how to avoid the exception propagating out to the TaskScheduler’s unhandled exception block. I too played with this for TOO LONG !

    Thanks =)

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.