Question

I have two asynchronous tasks that need to get completed - say they're "make a pie" (make) and "bake a pie" (bake). I also need to cleanup after everything's done - "clean the kitchen" (cleanup). bake depends on make, and I want to cleanup when things are done. However, there are a few complications:

  • If some nice person has already made the pie, all I have to do is bake.
  • If a problem happens during make - maybe I get tired of this whole pie business - I need to cleanup.
  • If a problem happens during bake - the oven explodes - I also need to cleanup.

My first pass looks like something like this:

func pieTime()
    if !pie.isMade()
        make()
    else if !pie.isBaked()
        bake()
    else
        cleanup(null)

func make()
    makeThePie(completion: {
        if pie.hasError()
            cleanup(error)
        else
            bake()
    })

func bake()
    bakeThePie(completion: {
        if pie.hasError()
            cleanup(error)
        else
            cleanup(null)
    })

func cleanup(error)
    if error != null
        shout("what the ***: %s", error.string())
    destroy(pie)

This isn't too bad, but my main problem with this approach is that there is no single exit point from the flow. cleanup gets called from a bunch of places. As the error handling gets more complicated, and you add more asynchronous calls to the chain, there are more and more exit points from the flow to keep track of. If I miss calling cleanup at any of them, the result is an unusable kitchen.

Basically, I'm looking for something like a do/catch/finally for asynchronous calls. The only thing I could come up with is a queue - queue up only the tasks you need and queue cleanup at the end - but it seems a bit too heavyweight for just two tasks, such as this instance. Is there a well-established pattern for this problem?

Était-ce utile?

La solution

You can use a promise chain like the idiomatic q one, roughly:

doSomethingThatReturnsAPromise
    .then(function (result, error) {
        if (error) {
            [the previous command failed in some way; handle it or throw.]
        }
    })
    .then([same again])
    […]
    .finally(function() {
        [global cleanup]
    })

The main difference from a tree of calls is that it's very intuitive to follow - there's no question of what the entry point is, there's no question of the sequence of the steps, and the cleanup happens unconditionally once the sequence is done.

Autres conseils

Why not

  • implement functions bake and make without any calls to cleanUp (but coherent error signaling like a specific exception)

  • implement a functional wrapper (a "decorator") which takes an arbitrary function as parameter, runs it asynchronously, catches any error exception and calls cleanup accordingly?

Then the place in code where bake and make (and other equivalent tasks) are started just have to use the wrapper to run the tasks. The final cleanup(null) should not be called by bake directly as well, but by the caller (for example, when he gets the event for all tasks having completed without any error).

The cleanup method can serve as a decorator for the make and bake command chain. In this case, the decorator serves the purpose of cleaning up. As the command chain executes, any error will move up the chain until the cleanup catches and cleans.

There is no reason to add in async as part of this chain since baking depends on making and error handling also depends on the result of making and possibly baking. If you wanted to run the entire chain async (making and baking several pies at once, then at the very top I would make an async callout and then look at the task result of the entire call chain as opposed to individual call outs.

Here's the chain (In C#):

var command = new PieCleanup(new MakePie(new BakePie()));
//You could run this async if many pies to make and bake
command.Execute(new Pie());

The Cleanup Decorator:

    public class PieCleanup : ICommand<Pie>
    {
        private readonly ICommand<Pie> _command;

        public PieCleanup(ICommand<Pie> command)
        {
            _command = command;
        }

        public void Execute(Pie pie)
        {
            try
            {
                _command.Execute(pie);
            }
            catch (Exception)
            {
                //TODO:  Cleanup
            }
        }
    }

Make Pie:

 public class MakePie : ICommand<Pie>
    {
        private readonly ICommand<Pie> _command;

        public MakePie(ICommand<Pie> command)
        {
            _command = command;
        }

        public void Execute(Pie pie)
        {
            //TODO: Make Pie
            //Now Run the next command (bake)
            _command.Execute(pie);
        }
    }

Bake Pie:

 public class BakePie : ICommand<Pie>
    {
        public void Execute(Pie pie)
        {
            //TODO: Bake Pie
        }
    }
Licencié sous: CC-BY-SA avec attribution
scroll top