Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a generic timeout object for various code blocks?

Tags:

c#

winforms

I have a series of code blocks that are taking too long. I don't need any finesse when it fails. In fact, I want to throw an exception when these blocks take too long, and just fall out through our standard error handling. I would prefer to NOT create methods out of each block (which are the only suggestions I've seen so far), as it would require a major rewrite of the code base.

Here's what I would LIKE to create, if possible.

public void MyMethod( ... )
{

 ...

    using (MyTimeoutObject mto = new MyTimeoutObject(new TimeSpan(0,0,30)))
    {
        // Everything in here must complete within the timespan
        // or mto will throw an exception. When the using block
        // disposes of mto, then the timer is disabled and 
        // disaster is averted.
    }

 ...
}

I've created a simple object to do this using the Timer class. (NOTE for those that like to copy/paste: THIS CODE DOES NOT WORK!!)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;

    public class MyTimeoutObject : IDisposable
    {
        private Timer timer = null;

        public MyTimeoutObject (TimeSpan ts)
        {
            timer = new Timer();
            timer.Elapsed += timer_Elapsed;
            timer.Interval = ts.TotalMilliseconds;

            timer.Start();
        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            throw new TimeoutException("A code block has timed out.");
        }

        public void Dispose()
        {
            if (timer != null)
            {
                timer.Stop();
            }
        }
    }

It does not work because the System.Timers.Timer class captures, absorbs and ignores any exceptions thrown within, which -- as I've discovered -- defeats my design. Any other way of creating this class/functionality without a total redesign?

This seemed so simple two hours ago, but is causing me much headache.

like image 954
Jerry Avatar asked Jan 04 '16 18:01

Jerry


1 Answers

OK, I've spent some time on this one and I think I have a solution that will work for you without having to change your code all that much.

The following is how you would use the Timebox class that I created.

public void MyMethod( ... ) {

    // some stuff

    // instead of this
    // using(...){ /* your code here */ }

    // you can use this
    var timebox = new Timebox(TimeSpan.FromSeconds(1));
    timebox.Execute(() =>
    {
        /* your code here */
    });

    // some more stuff

}

Here's how Timebox works.

  • A Timebox object is created with a given Timespan
  • When Execute is called, the Timebox creates a child AppDomain to hold a TimeboxRuntime object reference, and returns a proxy to it
  • The TimeboxRuntime object in the child AppDomain takes an Action as input to execute within the child domain
  • Timebox then creates a task to call the TimeboxRuntime proxy
  • The task is started (and the action execution starts), and the "main" thread waits for for as long as the given TimeSpan
  • After the given TimeSpan (or when the task completes), the child AppDomain is unloaded whether the Action was completed or not.
  • A TimeoutException is thrown if action times out, otherwise if action throws an exception, it is caught by the child AppDomain and returned for the calling AppDomain to throw

A downside is that your program will need elevated enough permissions to create an AppDomain.

Here is a sample program which demonstrates how it works (I believe you can copy-paste this, if you include the correct usings). I also created this gist if you are interested.

public class Program
{
    public static void Main()
    {
        try
        {
            var timebox = new Timebox(TimeSpan.FromSeconds(1));
            timebox.Execute(() =>
            {
                // do your thing
                for (var i = 0; i < 1000; i++)
                {
                    Console.WriteLine(i);
                }
            });

            Console.WriteLine("Didn't Time Out");
        }
        catch (TimeoutException e)
        {
            Console.WriteLine("Timed Out");
            // handle it
        }
        catch(Exception e)
        {
            Console.WriteLine("Another exception was thrown in your timeboxed function");
            // handle it
        }
        Console.WriteLine("Program Finished");
        Console.ReadLine();
    }
}

public class Timebox
{
    private readonly TimeSpan _ts;

    public Timebox(TimeSpan ts)
    {
        _ts = ts;
    }

    public void Execute(Action func)
    {
        AppDomain childDomain = null;
        try
        {
            // Construct and initialize settings for a second AppDomain.  Perhaps some of
            // this is unnecessary but perhaps not.
            var domainSetup = new AppDomainSetup()
            {
                ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
                ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
                LoaderOptimization = LoaderOptimization.MultiDomainHost
            };

            // Create the child AppDomain
            childDomain = AppDomain.CreateDomain("Timebox Domain", null, domainSetup);

            // Create an instance of the timebox runtime child AppDomain
            var timeboxRuntime = (ITimeboxRuntime)childDomain.CreateInstanceAndUnwrap(
                typeof(TimeboxRuntime).Assembly.FullName, typeof(TimeboxRuntime).FullName);

            // Start the runtime, by passing it the function we're timboxing
            Exception ex = null;
            var timeoutOccurred = true;
            var task = new Task(() =>
            {
                ex = timeboxRuntime.Run(func);
                timeoutOccurred = false;
            });

            // start task, and wait for the alloted timespan.  If the method doesn't finish
            // by then, then we kill the childDomain and throw a TimeoutException
            task.Start();
            task.Wait(_ts);

            // if the timeout occurred then we throw the exception for the caller to handle.
            if(timeoutOccurred)
            {
                throw new TimeoutException("The child domain timed out");
            }

            // If no timeout occurred, then throw whatever exception was thrown
            // by our child AppDomain, so that calling code "sees" the exception
            // thrown by the code that it passes in.
            if(ex != null)
            {
                throw ex;
            }
        }
        finally
        {
            // kill the child domain whether or not the function has completed
            if(childDomain != null) AppDomain.Unload(childDomain);
        }
    }

    // don't strictly need this, but I prefer having an interface point to the proxy
    private interface ITimeboxRuntime
    {
        Exception Run(Action action);
    }

    // Need to derive from MarshalByRefObject... proxy is returned across AppDomain boundary.
    private class TimeboxRuntime : MarshalByRefObject, ITimeboxRuntime
    {
        public Exception Run(Action action)
        {
            try
            {
                // Nike: just do it!
                action();
            }
            catch(Exception e)
            {
                // return the exception to be thrown in the calling AppDomain
                return e;
            }
            return null;
        }
    }
}

EDIT:

The reason I went with an AppDomain instead of Threads or Tasks only, is because there is no bullet proof way for terminating Threads or Tasks for arbitrary code [1][2][3]. An AppDomain, for your requirements, seemed like the best approach to me.

like image 102
Frank Bryce Avatar answered Sep 23 '22 19:09

Frank Bryce