1

I have a call to an unmanaged code library that hangs if passed an incorrect parameter.

IWorkspaceFactory workspaceFactory = _workspaceFactory.OpenFromString(connectionString);

If connectionString has an invalid value, then OpenFromString hangs. If I break execution, I can see from the call stack that control is still with the unmanaged library. Unfortunately, this method doesn't offer a timeout. What I need to do is, within my managed code, cancel the call after a certain timeout period. I've tried a couple of approaches, including the following:

Task<IWorkspace> task = new Task<IWorkspace>(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}
IWorkspace workspace = task.Result;

In this case, task.Wait() always returns false, and task.Result() is always null, even if a valid value for connectionString is passed.

I've also tried the following approach:

IWorkspace workspace = null;
Thread thread = new Thread(() =>
    {
        workspace = _workspaceFactory.OpenFromString(connectionString);
    });
thread.Start();
if (!thread.Join(10000))
{
    throw new TimeoutException();
}

In this case, if a valid value for connectionString is passed, execution works as expected, but if an invalid value is passed, the execution still hangs in the unmanaged code, and thread.Join() never returns.

Any suggestions on how to do this?

noseratio
  • 59,932
  • 34
  • 208
  • 486
Jonathan Bailey
  • 306
  • 2
  • 13
  • You are not waiting long enough, dbase connections attempts operate with network timeout values as a minimum, you must make it at least a minute. Giving up earlier makes your program the source of the problem instead of the dbase provider. If it still doesn't give you an exception then first ask yourself if you really want to support such a crummy provider, next ask yourself if there's any point left in keeping your program running when it can't get to the data. Terminating the program gets rid of that thread. – Hans Passant Mar 30 '14 at 17:08
  • Pretty sure I'm waiting long enough. Also, it is a known issue with the provider. Not supporting the provider isn't an option. Terminating the program really isn't a solution. – Jonathan Bailey Mar 30 '14 at 18:36

3 Answers3

1

In the first code fragment, you don't event start a task:

Task<IWorkspace> task = new Task<IWorkspace>(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}

You'd need to start it first with task.Run(). Check "Task.Factory.StartNew" vs "new Task(...).Start" and and Task.Run vs Task.Factory.StartNew.

Anyway, this won't solve the original problem:

if a valid value for connectionString is passed, execution works as expected, but if an invalid value is passed, the execution still hangs in the unmanaged code

You're dealing with some legacy and apparently buggy code here. Fixing it on the unmanaged side would be the best option, but presumably you don't have access to the sources.

You could use the technique described by Stephen Toub's in his "How do I cancel non-cancelable async operations?" However, in the scenario you described, you may exhaust the thread pool very quickly, not mentioning other system resources potentially being acquired by OpenFromString.

I think the best you could do in this case is to offload the WorkspaceFactory object to a separate helper process (rather than a thread within your own process) and call it via remoting. If the call times out, you'd kill and restart the helper process. The OS would do the proper job on cleaning up the resources.

You could wrap this process as self-hosted WCF service, or you could run it as an out-of-proc COM singleton.

Updated:

I understand your point about leaving threads running, but I think I can work with this

If you're happy to leave a pool thread blocked indefinitely, do just this:

Task<IWorkspace> task = Task.Run(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
if (!task.Wait(10000))
{
    throw new TimeoutException();
}
IWorkspace workspace = task.Result;

I have another concern though: your _workspaceFactory may not be thread-safe, which is often the case with legacy code. I.e., it may expect to be called only on the same thread it was originally created.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
1

You should find out why the library hangs indefinitely, that sounds like a serious error. Perhaps there is a way to validate the connectionString and throw an exception?

That aside, there is an extension method that Stephen Toub wrote in his article .NET Memory Allocation Profiling with Visual Studio 2012 that may help you achieve what you want.

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

The method creates a proxy task that can be cancelled, which wraps the original task. The extension method creates another task with a TaskCompletionSource<bool> and registers it with the CancellationToken, it then uses Task.WhenAny to await completion of either of the tasks.

The end result is that cancelling via CancellationToken will cancel the proxy task whilst the original task will keep running in the background and will consume a thread. If the unmanaged library hangs forever this thread will be consumed forever, which is really bad. But if the operation was to eventually timeout, this may be an acceptable solution. As far as the client is concerned the task will appear cancelled.

So you can add a method that returns a task:

public Task<IWorkspace> GetWorkscpaceAsync(string connectionString)
{
    Task<IWorkspace> task = new Task<IWorkspace>.Run(() => 
    { 
        return _workspaceFactory.OpenFromString(connectionString); 
    });
}

And then use the extension method like this:

var tcs = new CancellationTokenSource(1000);
IWorkspace result = await GetWorskpaceAsync(connectionString).WithCancellation(tcs.Token);
NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • I did mention this method in [my answer](http://stackoverflow.com/a/22751230/1768303) and explained why it might be not good enough in this case. – noseratio Mar 31 '14 at 04:03
  • Yours was a good answer I though, I just wanted to show the `WithCancellation` extension method which I didn't see in any of the articles you mentioned. – NeddySpaghetti Mar 31 '14 at 04:03
  • Stephen has a [dedicated blog entry](http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx) for this which I'm referring to. – noseratio Mar 31 '14 at 04:08
  • Ah yes, it is in there as well. Probably your link is more appropriate. I saw it in [.NET Memory Allocation Profiling with Visual Studio 2012](http://blogs.msdn.com/b/dotnet/archive/2013/04/04/net-memory-allocation-profiling-with-visual-studio-2012.aspx). +1 – NeddySpaghetti Mar 31 '14 at 04:15
  • Your link brings up another interesting pattern I haven't seen before: `WithCancellation2`. It wouldn't help much here but it's still cool - tks. – noseratio Mar 31 '14 at 04:32
  • @Noseratio Please bear with me, as I've not used the asynchronous programming model much at all. First, I think that this approach is workable. I understand your point about leaving threads running, but I think I can work with this. I get a couple of compile errors using the sample code: In _GetWorkspaceAsync_, I get an error on `Task.Run...`: "'System.Threading.Tasks.Task.Run(System.Action)' us a 'method' but is used like a 'type'. Also, in my own method: `IWorkspace result = await.GetWorkspaceAsync...` "The 'await' operator can only be used within an async method." – Jonathan Bailey Mar 31 '14 at 13:52
  • @SpatialBridge, check my [update](http://stackoverflow.com/a/22751230/1768303) regarding this. – noseratio Mar 31 '14 at 21:52
0

I you have two threads:

  1. From start

  2. The new, which calls the _workspacefactory.

After you have started thread 2 let thread 1 wait x milliseconds. If the _workspacefactory thread succeeds let it change a shared variable to true. When thread one continues after wait let it check the common variable. If the variable is false kill thread 2.

Anders Finn Jørgensen
  • 1,275
  • 1
  • 17
  • 33
  • I tried this approach too, but the _workspace_ is always `null`, even when passed a valid _connectionString_. `IWorkspace workspace = null; Thread thread = new Thread(() => { workspace = _workspaceFactory.OpenFromString(connectionString); }); thread.Start(); Thread.Sleep(10000); if (workspace == null) { throw new TimeoutException(); }` – Jonathan Bailey Mar 30 '14 at 16:32
  • There is an issue about sharing variables between threads. Normally you should use a delegate to transfer e.g. the workspace. I doesn't explain why it work with a proper connectionstring and not with an invalid one. maybe you could rewrite your code using a delegate? – Anders Finn Jørgensen Mar 30 '14 at 23:44