0

An answer in this post

suggests using a Timer to time out a connection attempt using ConnectAsync. I'm unsure how to handle disposal of the Socket if that timeout occurs.

Consider the following example

public static void Main (string[] args)
{
    IPEndPoint ep = new IPEndPoint (new IPAddress (new byte []{ 127, 0, 0, 1 }), 9042);
    var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
    var connectTimeoutMs = 50;
    var tcs = new TaskCompletionSource<bool> ();
    new Timer ((t) => {
        tcs.SetException(new ConnectionTimeoutException());
        ((Timer) t).Dispose();
    }).Change (connectTimeoutMs, Timeout.Infinite);

    var eventArgs = new SocketAsyncEventArgs { RemoteEndPoint = ep };
    eventArgs.Completed += Finisher (tcs);

    if (socket.ConnectAsync (eventArgs)) {
        // dealt synchronously
    }
    try {
        tcs.Task.Wait (); // actually a ContinueWith 
    } catch (AggregateException e) {
        if (e.InnerException is ConnectionTimeoutException) {
            socket.Dispose (); // is this the appropriate place for this?
        }
    }
    Thread.Sleep (10000);
}
public static EventHandler<SocketAsyncEventArgs> Finisher(TaskCompletionSource<bool> tcs) {
    return (sender, e) => {
        if (e.SocketError != SocketError.Success) {
            tcs.TrySetException (new SocketException ((int)e.SocketError));
        } else {
            tcs.TrySetResult (true);
        }
        e.Dispose ();
    };
}
class ConnectionTimeoutException : SocketException {
    public ConnectionTimeoutException(): base((int) SocketError.TimedOut){}
}

On Mono-3.10.0, if the timeout occurs, this causes a fatal (uncaught) ObjectDisposedException

Unhandled Exception:
System.ObjectDisposedException: The object was used after being disposed.
  at System.Net.Sockets.Socket.EndConnect (IAsyncResult result) [0x00016] in /home/user/dev/mono-3.10/mono-3.10.0/mcs/class/System/System.Net.Sockets/Socket.cs:1274 
  at System.Net.Sockets.SocketAsyncEventArgs.ConnectCallback (IAsyncResult ares) [0x00000] in /home/user/dev/mono-3.10/mono-3.10.0/mcs/class/System/System.Net.Sockets/SocketAsyncEventArgs.cs:260 
  at System.Net.Sockets.SocketAsyncEventArgs.DispatcherCB (IAsyncResult ares) [0x000aa] in /home/user/dev/mono-3.10/mono-3.10.0/mcs/class/System/System.Net.Sockets/SocketAsyncEventArgs.cs:234 

because the call to

socket.Dispose()

actually completes the SocketAsyncEventArgs, but first validates that the corresponding Socket wasn't disposed. This was "corrected" in newer versions of Mono, but it got me thinking whether this is the appropriate pattern for timing out a connection attempt on a Socket. Is it? And should the Completed event be raised on Dispose of the underlying Socket (with SocketError.Success, no less)?


Solution looks like

IPEndPoint ep = new IPEndPoint (new IPAddress (new byte []{ 127, 0, 0, 1 }), 9042);
var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
var connectTimeoutMs = 50;
var connectTask = Task.Factory.FromAsync(
                (ipEndPoint, callback, state) => ((Socket)state).BeginConnect(ipEndPoint, callback, state),
                asyncResult => ((Socket)asyncResult.AsyncState).EndConnect(asyncResult),
                ep, socket);
var timeoutTask = Task.WhenAny(connectTask, Task.Delay(Options.connectTimeoutMs));
if (await timeoutTask != connectTask) {
    throw new SocketException((int)SocketError.TimedOut);
}
await connectTask;
Savior
  • 3,225
  • 4
  • 24
  • 48
  • If your code will be disposing sockets for which outstanding operations are still in progress, it's your job to catch `ObjectDisposedException` and handle it correctly (e.g. clean up any data associated with the incomplete socket operation). – Peter Duniho Jun 16 '17 at 19:16
  • @PeterDuniho I can't. As I mentioned, Mono checks for disposed and throws that `ObjectDisposedException` in its own threads, it's not propagated to any place that my application code has access to, which is also why it goes unhandled and kills the process. – Savior Jun 16 '17 at 19:55
  • If Mono is calling `EndConnect()` on your behalf, that suggests to me that the `ConnectAsync()` method is provided for compatibility reasons, not because it's actually useful. So, you should just use `BeginConnect()` yourself instead, which should give you direct access to the exception thrown by `EndConnect()`. – Peter Duniho Jun 16 '17 at 20:36
  • @PeterDuniho So what I have in the `Finisher` should instead be in the `BeginConnect`'s `AsyncCallback`, and also call `EndConnect` explicitly (and handle exceptions)? – Savior Jun 16 '17 at 21:47
  • Yes. Or alternatively (not sure if this is available in Mono) consider using `Task.FromAsync()`, which wraps an old APM style call (i.e. `BeginXXX`/`EndXXX`) in a `Task`. Then you wouldn't need to create your own `TaskCompletionSource`. If you did that, I believe the `ObjectDisposedException` would be propagated cleanly to your `ContinueWith()`. – Peter Duniho Jun 16 '17 at 21:53
  • @PeterDuniho God, that makes for some beautiful code with `Task.WhenAny` and `Task.Delay`. Thank you! – Savior Jun 16 '17 at 22:24

0 Answers0