3
\$\begingroup\$

I've a bunch of sequential activities. These are long running. Once an activity is complete, it can't be rolled back. Now something deep down the line fails. Now I've few options

  1. Report this to end user and ask him to fix something. And then Retry same thing.
  2. End user has no clue on what to do. He can choose to ignore. (Just retry won't fix things)
  3. Abort/Pause? Now this is something tricky. This will stop the program. If program is rerun, it should resume from this failed point and not from the starting point.

I'm thinking to implement this by defining a custom exception, Action delegate set, coming from exception object. So my question is Do you see any issue with this pattern? Any better way to do the same?

[Serializable]
public class SafetyNetException : Exception
{
    public SafetyNetException(SafetyNet callBack) { Net = callBack; }
    public SafetyNetException(SafetyNet callBack, string message) : base(message) { Net = callBack; }
    public SafetyNetException(SafetyNet callBack, string message, Exception inner) : base(message, inner) { Net = callBack; }
    protected SafetyNetException(
      System.Runtime.Serialization.SerializationInfo info,
      System.Runtime.Serialization.StreamingContext context)
        : base(info, context) { }

    public SafetyNet Net { get; private set; }
}

public class SafetyNet
{
    public Action Abort;

    public Action Retry;

    public Action Ignore;
}

[TestClass()]
public class SafetyNetTest
{
    [TestMethod()]
    public void APIUsageTest()
    {
        try
        {
            DoSomething();
        }
        catch (SafetyNetException targetException)
        {
            Debug.WriteLine(targetException.Message);

            SafetyNet target = targetException.Net;
            switch (AskUser())
            {
                case "r":
                    target.Retry();
                    break;
                case "a":
                    target.Abort();
                    break;
                case "i":
                    target.Ignore();
                    break;
            }
        }
    }

    private void DoSomething()
    {
        try
        {
            // try something
        }
        catch (Exception ex)
        {
            SafetyNet net = new SafetyNet();
            net.Abort = () => DoSomething_Abort();
            net.Retry = () => DoSomething();
            net.Ignore = () => DoNextThing();

            throw new SafetyNetException(net, "Ask user", ex);
        }
    }
\$\endgroup\$
2
  • 2
    \$\begingroup\$ MS-DOS' most infamous error message. Infamous because no user could ever figure out what the proper response might be. There wasn't one :) \$\endgroup\$
    – Hans Passant
    Commented Apr 19, 2012 at 19:56
  • \$\begingroup\$ maybe look at this link to WF persistence services to see if it addresses similar concerns msdn.microsoft.com/en-us/library/ms734764(v=vs.90).aspx \$\endgroup\$ Commented Apr 19, 2012 at 20:12

2 Answers 2

5
\$\begingroup\$

You really shouldn't be using exceptions to control the flow of your program's logic. What you're trying to do tends to work much better as a simple state machine that, when it fails, asks whoever is listening what it should do and responds accordingly. Here's a very rough example (which hasn't been tested, is not threadsafe, etc).

public interface ISequentialActivity
{
    bool Run();
}

public enum UserAction
{
    Abort,
    Retry, 
    Ignore
}

public class FailureEventArgs
{
    public UserAction Action = UserAction.Abort;
}

public class SequentialActivityMachine
{
    private Queue<ISequentialActivity> activities = new Queue<ISequentialActivity>();
    public event Action<FailureEventArgs> OnFailed;    
    protected void PerformOnFailed(FailureEventArgs e)
    {
        var failed = this.OnFailed;
        if (failed != null)
            failed(e);
    }
    public void Add(ISequentialActivity activity) { this.activities.Enqueue(activity); }
    public void Run()
    {
        while (this.activities.Count > 0)
        {
            var next = activities.Peek();
            if (!next.Run())
            {
                var failureEventArgs = new FailureEventArgs();
                PerformOnFailed(failureEventArgs);

                if (failureEventArgs.Action == UserAction.Abort)
                    return;
                if (failureEventArgs.Action == UserAction.Retry)
                    continue;
            }

            activities.Dequeue();
        }
    }
}

In this example, classes would encapsulate sections of retry-able logic by implementing ISequentialActivity, returning true from their Run() method if they succeeded and false if they failed in some way. Failure would then be pushed up to the listener through the event for decision making.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks! This makes sense. Few questions: The need var failed = this.OnPerformed is thread safety thing? And going by this way, all actions will have to go via this Machine class. Is there any other way where .NET framework can handle that. System.Transaction namespace? (or like my exception approach above) \$\endgroup\$
    – Ankush
    Commented Apr 20, 2012 at 6:44
  • \$\begingroup\$ @Ankush The var failed = this.OnPerformed is about thread safety, in that it pushes potential race conditions to the listener. Eric Lippert has a good article on that. \$\endgroup\$ Commented Apr 20, 2012 at 16:05
  • \$\begingroup\$ @Ankush And you're correct that all actions would have to be run through this class for appropriate state transition semantics to be included. I don't know of anything inherently available in the .Net Framework that will handle this kind of thing for you, but as you saw it's not difficult to build. The System.Transactions namespace is mainly for interop with other systems that provide transactional support and I don't think it will be what you're looking for, but you're welcome to investigate and see if it would work for your specific situation. \$\endgroup\$ Commented Apr 20, 2012 at 16:11
0
\$\begingroup\$

I would suggest that the outer routine should receive a HandleError delegate which is called when a problem is detected, before an exception is thrown by, or allowed to propagate beyond, anything but the lowest-level code. The code within that delegate can hopefully receive as parameters whatever information is needed to proceed sensibly from that point; the caller of the delegate can then--based upon the return value from the delegate--either retry the operation or proceed as well as possible without it; alternatively, the delegate may throw an exception to cause the outer-scope action to be aborted (possibly after recording somewhere information about how or where the action should be restarted).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.