3

Basically I need to make a remote request using a vendor's .Net SDK for some information. Their SDK has no async implementations on their methods so I am trying to come up with something on my own. I bascially want to fire off this request to a synchronous method, and wait on it for only a certain amount of time. If the request takes too long, I need to act and report that down to the client in our web app.

I'm wondering if this is the best way to do this, or is there a better way? The code below is a service method that is called from a Controller action.

    public async Task<bool> SignersAdded(string packageId)
    {
        var timeout = 5000;

        var task = Task.Run(() =>
        {
            var package = _eslClient.GetPackage(new PackageId(packageId));
            return package != null && package.Documents.Values.Any(x => x.Signatures.Any());
        });

        var stopwatch = Stopwatch.StartNew();

        while (!task.IsCompleted)
        {
            if (stopwatch.ElapsedMilliseconds >= timeout)
                return false;
        }

        return false;
    }
Ryan
  • 31
  • 1
  • Why not pass a `CancellationToken` with a timeout instead of all the stopwatch stuff? The `while` turns your async code into blocking calls anyway. You also need to use `await` for this to be correct... – Ron Beyer Jul 16 '15 at 18:06
  • It's not really clear what you're trying to do here but it sounds like you have misunderstood the usage of async/await. – Ant P Jul 16 '15 at 19:41
  • What kind of client does this SDK make? You should be able to specify the timeout. For example with soap you can specify a timeout https://msdn.microsoft.com/en-us/library/microsoft.web.services3.messaging.soapclient.timeout.aspx – Andres Castro Jul 16 '15 at 21:06
  • Does their SDK offer any other form of asynchronous APIs, like BeginXXX/EndXXX APM pattern or event-based AEP pattern APIs? – noseratio Jul 17 '15 at 02:40

4 Answers4

0

Task.Wait has an overload that takes an int which defines timeout.

public Task<bool> SignersAdded(string packageId)
{
    var timeout = 5000;

    var task = Task.Run(() =>
    {
        var package = _eslClient.GetPackage(new PackageId(packageId));
        return package != null && package.Documents.Values.Any(x => x.Signatures.Any());
    });

    if(!task.Wait(1000 /*timeout*/))
    {
        // timeout
        return false;
    }
    return task.Result;
}
MarcinJuraszek
  • 124,003
  • 15
  • 196
  • 263
  • The problem with this is that it may exhaust the ASP.NET thread pool very quickly, by leaving those `GetPackage` calls hanging on pool thread. – noseratio Jul 17 '15 at 02:46
0

Your method doesn't await on anything, so it runs synchronously. Also, your while loop will spin the CPU, blocking the calling code until the task is complete.

A better approach might be this:

var task = Task.Run(/* your lambda */)
var finishedTask = await Task.WhenAny(Task.Delay(timeout), task);
return finishedTask == task;

This way we create a separate delay task for that time and we await until the first task is complete. This will run in a truly asynchronous manner - there is no while loop that will burn CPU cycles.

(The above assumes timeout is in milliseconds. If not, then use an overload to Delay taking a TimeSpan argument instead.)

Erik
  • 5,355
  • 25
  • 39
  • 2
    Should also be noted that `Task.WhenAny` doesn't cancel the task, it just returns when the first one completes. – Ron Beyer Jul 16 '15 at 18:20
0

You are correct: start a task that calls GetPackage. After that you can continue doing other things.

After a while when you need the result you can wait for the task to complete. However you don't have to do Task.Wait. It is much easier to use async / await.

To do this, you have to do three things:

  • Declare your function async
  • Instead of void return Task and instead of type TResult return Task<TResult>. You already did that.
  • Instead of waiting for the task to finish use await

Your function would look much simpler:

public **async** Task<bool> SignersAdded(string packageId)
{
   var timeout = TimeSpan.FromSeconds(5);
   var task = Task.Run(() =>
   {
      var package = _eslClient.GetPackage(new PackageId(packageId));
      return package != null
          && package.Documents.Values
                    .Any(x => x.Signatures.Any());
    });

    // if desired you can do other things here
    // once you need the answer start waiting for it and return the result:
    return await Task;
}

if you have a function that returns TResult the async version of it returns Task<TResult>.

the return value of await Task<TResult> is TResult

However, if you want to be able to wait with a timeout you can do the following:

var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1);
// cancel after 1 second
try
{
    return await task.Run( () => ..., tokenSource.Token);
}
catch (OperationCanceledException exc)
{
    // handle timeout
}
finally
{
    // do necessary cleanup
}

The disadvantage of making your function async is that all callers also have to be async and all have to return Task or Task<TResult>. There is one exception:

An event handler can be async but may return void

Example:

private async void OnButton1_clicked(object sender,   )
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
0

Look at the TaskCompletionSource and the CancellationToken class. Samples here: Timeout an async method implemented with TaskCompletionSource or How to cancel a TaskCompletionSource using a timout

Community
  • 1
  • 1
Tamás Deme
  • 2,194
  • 2
  • 13
  • 31