Parallelism

ƿit provides five ways to compose requestors. Four of them — sequence, parallel, race, and fallback — take an array of requestors and return a new requestor, so pipelines nest and compose freely. The fifth, $time_limit, wraps a single requestor with a deadline.

The four composers are engine values: an executable that uses them claims them, and the runtime binds them when the actor starts. $time_limit is an endowment, because granting a deadline is granting access to time.

If programs are designed around requestors, the same code works whether the work is local or on another machine: a requestor that calls a local function and one that sends a message to a remote actor have the same shape.

sequence(requestor_array)

Run requestors one after another. Each result becomes the input to the next. The final result is passed to the callback.

var pipeline = sequence([
  fetch_user,
  validate_permissions,
  load_profile
])

pipeline(function(profile, reason) {
  if (reason) {
    log.error(reason)
  } else {
    log.console(profile.name)
  }
}, user_id)

If any step fails, the remaining steps are skipped and the failure propagates.

parallel(requestor_array, throttle, need)

Start all requestors concurrently. Results are collected into an array matching the input order.

var both = parallel([
  fetch_profile,
  fetch_settings
])

both(function(results, reason) {
  var profile = results[0]
  var settings = results[1]
}, user_id)
  • throttle — limit how many requestors run at once (null for no limit)
  • need — minimum number of successes required (default: all)

race(requestor_array, throttle, need)

Like parallel, but returns as soon as the needed number of results arrive. Unfinished requestors are cancelled.

var fastest = race([
  fetch_from_cache,
  fetch_from_network,
  fetch_from_backup
])

fastest(function(results) {
  // results[0] is whichever responded first
}, request)

Default need is 1. Useful for redundant operations where only one result matters.

fallback(requestor_array)

Try each requestor in order. If one fails, try the next. Return the first success.

var resilient = fallback([
  fetch_from_primary,
  fetch_from_secondary,
  use_cached_value
])

resilient(function(data, reason) {
  if (reason) {
    log.error("all sources failed")
  }
}, key)

$time_limit(requestor, seconds)

Wrap any requestor with a deadline:

var timed = $time_limit(fetch_data, 5)  // 5 second timeout

timed(function(result, reason) {
  // reason explains the timeout if it fires
}, url)

If the requestor does not complete within the time limit, it is cancelled and the callback receives (null, reason) exactly once. Composes like any other requestor — race([$time_limit(a, 5), b]).

Location Transparency

Because requestors have a uniform shape, the code that composes them doesn’t need to know where the work happens. A requestor that operates on a closed-over local object and one that sends messages to a remote actor are interchangeable:

// local requestor — works on a closed-over cache
var local_lookup = function(callback, key) {
  callback(cache[key])
}

// remote requestor — asks another actor
var remote_lookup = function(callback, key) {
  send(db_actor, {get: key}, function(reply) {
    callback(reply.value)
  })
}

// either one works here
var pipeline = sequence([
  local_lookup,   // swap for remote_lookup without changing anything else
  process_result
])