Implementing Actors in JavaScript

JavaScript does not exactly have an ideal semantic model for implementing Actors. Execution is single-threaded and sequential. Most objects are mutable by default. However, it has the advantage of being the most available and widely used computer language, so it seems worthwhile to show how actors can be used in this environment.

Asynchronous Programming

JavaScript programming has introduced many developers to the idea of asynchronous programming through the use of “callbacks”. Node.js popularized this style of programming for the server-side. Consider a typical synchronous call to an object method producing a return value.

var account = {
    balance: 0,
    deposit: function deposit(amount) {
        this.balance += amount;
        return this.balance;
    }
};

var balance = account.deposit(42);
console.log('Balance:', balance);

Contrast that with an asynchronous callback generating a result.

var account = {
    balance: 0,
    deposit: function deposit(amount, callback) {
        this.balance += amount;
        callback(this.balance);
    }
};

account.deposit(42, function (balance) {
    console.log('Balance:', balance);
});

So far, this just looks like a complicated way to write sequential code. However, in real applications the expression “this.balance += amount” would be a call to update the account balance in some persistent storage—a process that would take some time and would block the browser (or the server in Node.js). The callback provides a way to specify what code should be executed after some long-running task completes.

invokeLater(callback)

A classic example of a callback-style API is setTimeout(), which returns immediately but schedules the execution of a callback function after some delay.

setTimeout(function callback() {
    console.log('World!');
}, 1000);
console.log('Hello,');

This code will print “Hello,” immediately, followed by “World!” about a second later.

Our first step toward actors is generalizing an asynchronous execution primitive. Essentially what we want is setTimeout() with no delay. Node.js calls this setImmediate(), but supports some additional functionality we don’t need, so we will introduce a streamlined version called invokeLater().

if (typeof setImmediate === 'function') {
    var invokeLater = function (callback) { setImmediate(callback); }
} else if (typeof setTimeout === 'function') {
    var invokeLater = function (callback) { setTimeout(callback, 0); }
}

This version will suffice for demonstration purposes. For a much more robust and complete implementation approach see Domenic Denicola’s polyfill for setImmediate().

Focusing on Send

Discussions with Carl Hewitt about mapping actor semantics to JavaScript led to a focus on the “send” primitive. An actor send is different than a callback invocation because the send only schedules the message for later delivery. Building on invokeLater() here is a simple implementation of send:

var send = function send(recipient, message, callback) {
    invokeLater(function () {
        var returnValue = recipient.behavior(message);
        callback(returnValue);
    });
};

The send() function returns immediately, but schedules the later execution of a function that invokes the behavior of a recipient actor, to deliver the message. Any returnValue produced by the behavior is passed as a parameter to the (synchronous) callback function.

Using this implementation, an actor is simply any object with a behavior method, as shown below. Note that inside the behavior, the keyword this refers to the recipient actor/object. We can use a factory function to construct new actor instances that follow this protocol.

var createAccount = function createAccount() {
    return {
        balance: 0,
        behavior: function (message) {
            if (message.type === 'deposit') {
                this.balance += message.amount;
                return this.balance;
            }
        }
    };
};

Each call to this factory creates an “Account” instance with an initial balance of zero. The behavior function is defined to look for deposit messages containing an amount to add to the balance. The resulting balance is returned, where it becomes the parameter to the callback function.

Here is an example usage of an Account actor:

var account = createAccount();

send(account, { type:'deposit', amount:42 }, 
    function (returnValue) {
        console.log('New balance is', returnValue);
    });

A new account is created and sent a deposit message. After the deposit is processed, the new balance is displayed on the console.

Exception Handling

Synchronous object method calls can return to the caller via two paths. Either they return a value, or throw an exception. When the call becomes asynchronous, it can still “return” a value (via the callback), but how should an exception be handled?

The convention that has emerged in the Node.js community is to signal exceptions through an initial parameter in the callback signature. If the first callback parameter is considered “true” by a Boolean test, then it represents an error condition. If the first callback parameter is considered “false” by a Boolean test, then the remaining parameter(s) constitute the return value for a successful operation.

Applying this convention to our send() primitive, we wrap the behavior invocation in a try/catch construct. If the behavior throws an exception, the callback is invoked with the error as the first (and only) parameter. Otherwise, the first parameter is false, and the returnValue becomes the second parameter.

var send = function send(recipient, message, callback) {
    invokeLater(function () {
        try {
            var returnValue = recipient.behavior(message);
            callback(false, returnValue);
        } catch (error) {
            callback(error);
        }
    });
};

Now we can safely use exceptions to report error conditions in actor behaviors. In the Account example, we throw an exception if the message is not understood.

var createAccount = function createAccount() {
    return {
        balance: 0,
        behavior: function (message) {
            if (message.type === 'deposit') {
                this.balance += message.amount;
                return this.balance;
            } else {
                throw new Error('Not Understood');
            }
        }
    };
};

Our callback function must also be modified to look for an error as the first parameter. Here we choose to re-throw the error from the callback.

var account = createAccount();

send(account, { type:'deposit', amount:42 }, 
    function (error, returnValue) {
        if (error) {
            throw error;
        }
        console.log('New balance is', returnValue);
    });

Focusing on send, we have shown a simple implementation of actors in JavaScript. All three actor primitives are supported by this implementation. “Create” is implemented by constructing an object with a behavior. “Send” is implemented as a global function. “Become” is implemented by assigning this.behavior the behavior to apply to subsequent messages.

Focusing on Create

Earlier collaboration with Tristan Slominski led to a different implementation approach, focusing on the “create” primitive. We wanted an actor reference to be a unique, unforgeable, opaque capability [1] to send the actor a message. In JavaScript, we represent this capability with a function that sends (asynchronous) messages to a specific actor.

var sponsor = function create(behavior) {
    var actor = function send(message) {
        invokeLater(function () {
            try {
                context.behavior(message);
            } catch (error) {
                fail(error);
            };
        });
    };
    var context = {
        self: actor,
        behavior: behavior
    };
    return actor;
};

The sponsor is also a capability. The capability to create a new actor. Again, we represent this capability with a function.

The create() function takes a behavior, which is a function that will process a message in the context of an actor. Inside the behavior, the keyword this refers to the context object, which is not accessible from outside the actor. The context maintains a reference to the actor’s send capability (this.self) and its current behavior (this.behavior).

If an exception occurs while an actor is handling a message, something is wrong. We do not use exceptions as a control-flow mechanism, so they usually indicate some kind of programming error. The fail() function defines a global exception-handling policy.

var fail = function fail(error) {
    console.log('Fail!', error);
};

This implementation simply reports the error to the system console.

Sponsored Account

Sponsored Accounts can also use a factory for creation, but the factory creates behavior functions, not actors.

var newAccount = function newAccount(balance) {
    return function accountBehavior(message) {
        // { type:'deposit', amount:number, ok:actor, fail:actor }
        if (message.type === 'deposit') {
            var newBalance = balance + message.amount;
            if (newBalance >= 0) {
                balance = newBalance;
                message.ok(balance);
            } else {
                message.fail({
                    error: 'Insufficient Funds',
                    account: this.self
                });
            }
        }
    };
};

Each time newAccount() is called, it creates a new accountBehavior(), with balance as private state bound by the closure. When an actor created with this behavior receives a deposit message, a new balance is calculated. If the new balance is zero or more, the updated balance is sent to the ok actor. If the new balance is negative, an error message is sent to the fail actor (including a reference to this.self, the actor reporting the error). Note how there are no return values or thrown exceptions here. All flow-control and data-flow is accomplished through asynchronous messages to actors.

var log = sponsor(function (message) {
    console.log(message);
});

We use the sponsor (the “create” capability) to create an actor called log, whose behavior is to print on the system console any message it receives. This actor represents a common logging channel that we can use to report any errors.

var account = sponsor(newAccount(13));

account({
    type: 'deposit',
    amount: 42,
    ok: sponsor(function (message) {
        console.log('New balance is', message);
    }),
    fail: log
});

We create an account actor with behavior generated by newAccount() and an initial balance of 13. We use the account actor reference (the “send” capability) to send an asynchronous deposit message to the account. The new balance will be sent to the ok actor, created anonymously here by the sponsor, with a behavior specified inline. The message “New balance is 55” should appear on the console.

Revocable Proxy

Perhaps a more interesting example is the “revocable proxy” capability security pattern. A revocable proxy is a transparent forwarder for messages to an actor. Proxy access can later be revoked, breaking the connection to the target actor. It is generally created to give controlled access to the target actor without revealing the actor’s real address.

var newProxy = function newProxy(actor) {
    return function proxyBehavior(message) {
        if (message === actor) {
            this.behavior = function () {};
        } else {
            actor(message);
        }
    };
};

The newProxy() function is a factory for revocable proxy behaviors. The target actor is bound privately in closure. Any message other than the actor address is forwarded to the target actor. We assume that the client of the proxy doesn’t know the actor address (or it wouldn’t need the proxy), so we interpret a message containing just the actor as a “poison pill”, indicating that the proxy should be revoked. Revocation is accomplished by replacing this.behavior with a function that ignores all subsequent messages.

var plog = sponsor(newProxy(log));

plog(1);
plog(2);
plog(log);  // revoke!
plog(3);

Now we can create a proxy for the previously-defined log actor and send it a series of messages. Once the “plog(log)” message is processed, the proxy will ignore all subsequent messages.

Focusing on create, we have shown a simple implementation of actors in JavaScript. All three actor primitives are supported by this implementation. “Create” is implemented as a global function. “Send” is implemented by constructing a unique send-capability for each actor. “Become” is implemented by assigning this.behavior the behavior to apply to subsequent messages. This approach is further elaborated and refined in the open-source TartJS project [2].

Summary

The JavaScript execution model uses a single run-to-completion thread, handling one event at a time. Asynchronous callbacks provide a way to cooperatively interleave the execution of multiple tasks. Actors can be implemented in this environment by building on this callback mechanism.

Sending a message to an actor schedules a later invocation of the actor’s behavior without blocking the sender. Message delivery events invoke the actor’s behavior using run-to-completion semantics. The actor can create new actors, send new (asynchronous) messages, and designate a new behavior for handling future messages. Thus we have shown how all three actor primitives can be implemented within the JavaScript run-time environment.

References

[1]
M. Miller. Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control. Doctoral Dissertation. John Hopkins. 2006.
[2]
D. Schumacher and T. Slominski. TartJS: JavaScript implementation of Tiny Actor Run-Time, https://github.com/organix/tartjs. 2013.
This entry was posted in Uncategorized and tagged , , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

3 Comments

  1. Erhan Gundogan
    Posted 2014-12-05 at 7:19 am | Permalink

    Very nice approach and article. Thanks for sharing :)

  2. Bjorn
    Posted 2016-01-21 at 3:50 am | Permalink

    Hi and thanks for great post.

    I would like to add multiplicity to the whole thing. As they say: one actor is no actor. I tried to add several account actors to your example (the send-focused one) and have them execute their behaviors for different time lengths. But they don’t seem to run async. They are scheduled async and fine with send(). But a fast actor scheduled to run immediately after a much slower one will run its callback after the slower.

    Is it only the scheduling of execution with send() that is async? And the executions of the actual behaviors will run synchronously in the order they were scheduled? Need something special be done with the actor for them to run async?

  3. Dale Schumacher
    Posted 2016-01-26 at 12:28 pm | Permalink

    It is a limitation (or feature) of JavaScript that there is only a single thread of control. All events are dispatch on the same thread, and each runs to completion before the next event is dispatched. Therefore, there is no true parallel execution in JavaScript. In addition, events posted with no time-delay (as in invokeLater) are dispatched in the order they are scheduled.

    The trick to getting some degree of concurrency in JavaScript is to break up any long-running algorithm into several small (quick) steps, connected together with async message passing. Actors can facilitate this in two different ways. First, by creating an actor for each step. Second, by modifying the behavior of a single actor, creating a simple state-machine. The key to all of this is the async send() primitive.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*