A Generic Timeout Helper

Another post that hopefully may come in handy to someone!

Some APIs, but not all, notably older ones, allow passing a timeout value for the maximum allowed duration of its completion. What for those that don’t have such parameter? Well, I wrote one helper method to assist in those cases. All it takes is an action and the desired time slot, and it will throw an exception if the action takes longer than it should, leaving it for us to deal with the situation. Here is the code:

public static class RestrainedExtensions
{
     private static void ExecuteRestrainedCommon(Task actionTask, TimeSpan maxDelay)
     {
         var delayTask = Task.Delay(maxDelay);
         var finishedTaskIndex = Task.WaitAny(actionTask, delayTask);
         if (finishedTaskIndex != 0)
         {
             throw new TimeoutException("Action did not finish in the desired time slot.");
         }
     }

     public static void ExecuteRestrained<T>(Func<T> func, TimeSpan maxDelay)
     {
         var executionTask = Task.Run(() =>
         {
             func();
         });
         ExecuteRestrainedCommon(executionTask, maxDelay);
     }

     public static void ExecuteRestrained(Action action, TimeSpan maxDelay)
     {
         var executionTask = Task.Run(() =>
         {
             action();
         });
         ExecuteRestrainedCommon(executionTask, maxDelay);
     } }

As you can see, the RestrainedExtensions class offers two overloads of the ExecuteRestrained method, this is to make our lives easier when we want to execute some method that has a return type or otherwise. Essentially what we do is, we create two tasks, one which just executes a delay and the other which tries to execute our action, and we wait for the first to complete; if this wasn’t our action, then we throw an TimeoutException.

An alternate implementation of ExecuteRestrained, making use of the new WaitAsync method, could be:

private static void ExecuteRestrainedCommon(Task actionTask, TimeSpan maxDelay)
{
actionTask.WaitAsync(maxDelay).ConfigureAwait(false).GetAwaiter().GetResult();
}

If the action to be awaited does not run in the desired time slot, WaitAsync throws a TimeoutException.

Here’s a simple usage:

RestrainedExtensions.ExecuteRestrained(Console.ReadLine, TimeSpan.FromSeconds(3));

As you can imagine, this gives the user 3 seconds to enter something at the console before throwing an exception. Simple enough, probably has something to it, but surely can be used in most cases! As always, feel free to share your thoughts! Winking smile

                             

6 Comments

  • This is a bit trickier than first appears.

    Most solutions like this have a subtle problem: they can leave dangling `Task.Delay` instances (which are essentially timers). Similar solutions have run into issues with dangling timers when used at scale.

    I also noticed your solution uses `Task.Run`, which may be surprising to users (especially the way the code will behave if you pass a Task-returning Func to `ExecuteRestrained`).

    If you haven't seen it, modern .NET now has a `Task.WaitAsync` method, which is a very clean implementation (no dangling timers).

  • @Hi, Stephen!
    An honour to have you here! :-)
    What you propose is I write something along these lines?

    var executionTask = Task.Run(() =>
    {
    action();
    });

    Task.Run(async () =>
    {
    await executionTask.WaitAsync(maxDelay);
    });

  • Your code says
    TimeSpan.FromSeconds(3))
    but your text says
    gives the user 5 seconds to ..

    hmm

  • FYI today's title on
    https://www.packtpub.com/free-learning
    is NH
    https://subscription.packtpub.com/book/programming/9781784393564
    where Ricardo Peres was primary reviewer. Being 2015 it cites your SF work but not your books
    Entity Framework Core Cookbook (2016)
    Modern Web Development with ASP.NET Core 3 (2020)

    would be interesting to hear your views today of EF8 vs NH vs Dapper.

  • This signature
    public static void ExecuteRestrained<T>(Func<T> func, TimeSpan maxDelay)

    returns void and seems to swallow the actual T returned by Func<T> or would caller obtain that byref on 1st param as an "out" ?

    guess needs some /// to explain!

  • @Dick: many thanks, fixed the 5 second thing, my bad!
    Regarding EF8 vs NH, there's a post coming out about the current state of EF8, which is essentially an update of https://weblogs.asp.net/ricardoperes/current-limitations-of-entity-framework-core. I have used Dapper and will probably say something about it in a series of posts to come.
    Regarding the swallowing of T, that's because it is irrelevant for this purpose, this extension is to be used when we want something to run for a certain period of time. But I guess we could have one overload that returns T, yes.
    Many thanks for your relevant feedback, much appreciated!

Add a Comment

As it will appear on the website

Not displayed

Your website