Automatically scheduled JS tasks—with joins!

When I spoke at the Mountain View JS meetup about upcoming features in JS the other week, I showed a slide demonstrating how you can build cooperative threads with generators, in order to do asynchronous I/O in sequence without having to chain callbacks. I got a great question about this: is it possible to do several operations concurrently but then block until they all finish? Or do you have to force every I/O operation into a sequence? In other words, can you implement joins?

So I figured it was time to make my jstask library a little more fully-featured. I’ve implemented a task scheduler, so that tasks don’t have to be manually resumed. And I’ve implemented join, so that you can chain some operations, but still run others concurrently. For example:

spawn(function() {
    var foo, bar, baz;
    yield join(spawn(function() { foo = yield read("foo.txt"); }),
               spawn(function() { bar = yield read("bar.txt"); }),
               spawn(function() { baz = yield read("baz.txt"); }));
    // ...
});

I’ve designed the library so that if a task throws an exception and another task is joined on it, the exception propagates to the joiner. So you can simply write:

spawn(function() {
    var foo, bar, baz;
    try {
        yield join(spawn(function() { foo = yield read("foo.txt"); }),
                   spawn(function() { bar = yield read("bar.txt"); }),
                   spawn(function() { baz = yield read("baz.txt"); }));
    } catch (e) {
        // ...
    }
    // ...
});

If any one of the subtasks throws an exception, the others are automatically cancelled and the exception propagates to the outer task.

This might not be the only policy that makes sense. I hope to keep experimenting with jstask, and it’s up on github and available for forking (no pun intended). Maybe we’ll discover other useful policies. This is one of the cool things about generators: they’re general and flexible enough to support multiple different libraries, policies and applications.

7 Responses to Automatically scheduled JS tasks—with joins!

  1. You say that if a subtask throws an exception, the others are automatically cancelled. What does that entail? If the other taks have some cleanup to perform (e.g. cancel a pending XMLHttpRequest), how does that work?

    Have you seen Stratified JavaScript (http://onilabs.com/presentations/OSCON2010/, crossbrowser-implementation: http://onilabs.com/docs)?

    It has a fork-join composition construct that takes the form:

    waitfor { A } and { B } and { C }

    As in jstask, if any of the tasks (or ‘strata’) A, B or C throw an exception, the others will be cancelled. This cancellation can be caught by try/retract block. So if A is something like:

    var request = new XMLHttpRequest();

    try {
    send_request_and_await_reply(request);
    }
    retract {
    request.cancel();
    }

    and B throws an exception before the request is complete, then the request will automatically get cancelled.

    In addition to fork-join, StratifiedJS also has an “alt composition” construct inspired by the Orc calculus (http://orc.csres.utexas.edu/):

    waitfor { A } or { B } or { C }

    This completes as soon as any of A,B or C complete and cancels the remaining still pending strata.

  2. You say that if a subtask throws an exception, the others are automatically cancelled. What does that entail? If the other taks have some cleanup to perform (e.g. cancel a pending XMLHttpRequest), how does that work?

    Good question! I’ve set it up so that a cancelled task is given the opportunity to do cleanup via finally blocks. The way this works is, the generator representing the task kernel is shut down via the generator’s close() method, which interrupts the body as if via an exception, so all pending finally blocks are executed.

    I haven’t really fleshed out the I/O API’s (so far I only have a simple read and sleep as proofs of concept) to do proper finalization.

    Have you seen Stratified JavaScript?

    No, thanks for the link. It sounds like Stratified JS uses a compiler. In my case, I’ve just written a library, which only relies on generators. Generators already exist in SpiderMonkey (i.e., in Firefox), and are a good candidate for standardization for the next version of the ECMAScript standard.

    Thanks for the reference!

    Dave

  3. PS Looking at the examples, I think it would be good to add another finalization mechanism for I/O libraries to register themselves with, so that users of e.g. read don’t have to write finally blocks to cancel the underlying XHR.

  4. Nice slides! I’ll experiment with incorporating some of the Stratified JavaScript abstractions into jstask.

    Thanks again,
    Dave

  5. > PS Looking at the examples, I think it would be good to add another finalization
    > mechanism for I/O libraries to register themselves with, so that users of e.g. read
    > don’t have to write finally blocks to cancel the underlying XHR.

    In StratifiedJS, where have the luxury of being able to add new keywords to JS, so we implemented a “using” construct that does this – see http://onilabs.com/stratifiedjs#using .
    It is similar to Python’s “with”, but of course “with” is already taken in JS :-)

    > Nice slides! I’ll experiment with incorporating some of the Stratified JavaScript
    > abstractions into jstask.

    Looking forward to seeing that!

  6. OT cond

    var x = cond {
    case y > 100 { “huge” }
    case y > 50 { “big” }
    default { “small” }
    }

    // suggestion
    var x = ?? {
    y > 100 : “huge”,
    y > 50 : “big”,
    … : “small”
    }

    n.b – comma delimits ??cond parts as with object literals. If comma is problematic maybe use semi-colon.

  7. RE cond:

    Looks like there are plans for ?? – and as default is a keyword (and more readable than the … rest operator):

    // revised suggestion
    var x = ?: {
    y > 100 : “huge”,
    y > 50 : “big”,
    default: “small”
    }

    Rationale:
    1. The ?: expresses a relationship to the existing ?: construct
    2. Succinct and readable. Easy to parse. No new keywords (which could be a factor).
    3. The body looks like an object literal – which is familiar. The way the comma operator works in object literals is familiar. ie. next part of the construct.
    If a user wants to use the ‘comma’ operator:
    y > 50: (a++, b + 40),

    Also – is there a case for an ‘elif’ keyword:

    if (x > 4) doit(x)
    elif (x > 2) doSomething(x)
    else doWhatever(x);

    There is some overlap with a ‘cond’ operator. And i appreciate the need to be parsimonious with new keywords. However:
    1. It makes code more succinct and readable (compared to chained ‘else if’).
    2. It’s easy to implement.
    3. If cond/?: is expression orientated then a statement focused construct would be useful.