Recursive deferred promises

Hey, "recursive deferred promises" might sound like a smashingly complex idea, but really, it isn't. Here's the what, why and how.

The promises pattern is very useful for asynchronous code which tends to lead to "arrow-head" formation in code structure. It allows to continually develop in a form that seems blocking, while handling both success and failure in a comfortable, sane manner. I'm sorry I was not aware of it when I gave my "Asynchronous Programming" talk in YAPC::EU 2012.

Fortunately for us, we are lucky to have such skilled, cool, and handsome people as Stevan Little and Paul Evans, to implement Promises and Future, respectively, for us in Perl. Nowadays for my more complicated projects, I use Promises with joy. Stevan and Paul, each in their developed modules, have provided numerous examples (and introductory text) to how this pattern works and how to use it in various situations. Also, they are (as always) more than helpful when asked for pointers.

Letting go of my crush of Stevan and Paul for a bit, I'd like to explain a specific issue that troubled me in a project, which lead me to write recursive promises. What is it, why did I need it and how did I accomplish it? I'll be using the Promises module, though the principle is exactly the same as in the Future module.

I have a project which deploys a new server. This requires raising a ticket to create an instance, monitoring the status, and then running commands on it sequentially. First command updates, the next installs some prereqs, the one after that installs the deployment infrastructure and the last one is run locally to deploy our code to the server. This clearly leads to the arrow-head format:


run_remote_command $host, 'aptitude update && aptitude safe-upgrade', sub {
    # check possible fail or succeed
    run_remote_command $host, 'aptitude install build-essential curl...', sub {
        # check possible fail or succeed
        run_remote_command $host, 'gem install chef...', sub {
            # check possible fail or succeed
            run_local_command 'chef solo cook ...', sub {
                # check possible fail or succeed
            }
        }
    }
};

Not so pretty, is it? Well, I can use promises to make it more manageable:


use Promises 'deferred';
my $def = deferred;
run_remote_command $host, 'aptitude update && aptitude safe-upgrade', sub {
    if ( my $result = shift ) {
        $def->resolve($result);
    } else {
        $def->reject("Failed update/safe-upgrade");
    }
};

$def->promise->then( sub {

my $def = deferred;
run_remote_command $host, 'aptitude install build-essential curl...', sub {

if ( my $result = shift ) {
$def->resolve($result);
} else {
$def->reject("Failed update/safe-upgrade");
}
} );
return $def->promise;
}, sub {
my $error = shift;
die $error;
} )->then( sub {
my $def = deferred;
run_remote_command $host, 'gem install chef...', sub {
if ( my $result = shift ) {
$def->resolve($result);
} else {
$def->reject("Failed installing chef");
}
} );
return $def->promise;
}, sub {
my $error = shift;
die $error;
} );
...

You get the idea, right? Create a deferred promise, resolve or reject it later, and pass back the promise itself. Use the promise returned to chain more promises with deferred promises that resolve or reject later. Very nice. If you have a hard time following this, try this cookbook.

My problem was having to hardcode all the commands and have a known list of commands in advance, in order to write a promise step for each. You cannot have an arbitrary number of promises steps. At least not too easily. I wanted an array of any number of commands that will be run separately in each promise step. My solution was a recursive deferred promises pattern (at least that's how I'm calling it for now - trademarked!), illustrated in this Github gist:

As you can see, I set up the first promise and chain it to a code reference that calls itself recursively until the array of actions is exhausted. A good point made by Nicholas Perez was that instead of defining a variable for the callback and only then defining it as a coderef (thus allowing it to call itself), I could have used the new __SUB__ option more modern Perls (since Perl 5.16) have. In production my code has to be more backwards compatible, so I had to use my method instead.

Epilogue, sort of:
The one thing I want you to take from this post is not that I'm cool (although I am, very much so - you should definitely consider being my friend), but rather that when we talk about creating cool stuff (re: John @genehack Anderson's talk on this), we talk about stuff like Future and Promises. That's an example of writing something awesome, even if we didn't invent the concept. If you look at the code of Promises, for example, it's not even complicated or hairy. It's short, clean and elegant, and it provides a great service for programmers, and a wonderful standing point when we want to promote Perl as a strong and modern language. "Futures/Promises? Oh yeah, we got that!"

4 Comments

This is cool stuff! I really am enjoying learning how to do async stuff.

For completeness (and possible inspiration) I want to point out Mojo::IOLoop::Delay, which prevents arrowheading in a similar way to promises. You make a list of callbacks and they are executed in order asynchronously, with the ability to pass arguments to the next part of the chain.

Cheers and happy Perling!

Actually, Future already has a way to handle this very thing. Any sort of while or foreach-type loop can be written using the repeat function in Future::Utils.

For instance, in your specific case you have a few command strings to run:

my @commands = (
   'aptitude update ...',
   'aptitude install ...',
   'gem install ...',
);

my $f = repeat_until_success {
my $command = shift;
return run_remote_command( $host, $command );
} foreach => \@commands;

$f->await;

The basic repeat function has ways to control the ending condition, or provide a list of values to iterate over.

I should also add: The code block you wrote here has a bug in it. Because the anonymous function contains a reference to itself, it forms a cycle that will never be collected and leak memory. To fix it, you'd have to remember to undef the variable:

my $url = shift @urls or undef($cb), return $cv->send;

This sort of pattern is too subtle in more complex cases; easier to use Future::Utils or perhaps something from CPS.

Since you're already working with AnyEvent, this functionality can be achieved with Condvars.

Leave a comment

About Sawyer X

user-pic Gots to do the bloggingz