Fibers

One of the most notable differences between Node.js and other languages is Node’s non-blocking IO, and resulting asynchronous programming model. While this has many advantages, it can often be challenging to work with complex callback structures – a phenomenon often termed “callback hell” or “pyramid of doom.”

Many different libraries and techniques exist to help programmers manage this complexity. While any can be used with Carbon.io, as it supports both synchronous and asynchronous programming styles, Carbon.io’s preferred technique is to use fibers.

While fibers provides basic coroutine support, Carbon.io implements the abstractions necessary to allow you as the programmer to more easily write non-blocking code in a synchronous style.

Consider this standard asynchronous operation:

1
2
3
4
5
6
7
fs.readFile(path, function(err, data) {
  if (err) {
    console.log(err)
  } else {
    console.log(data)
  }
})

With fibers you can do this:

1
2
3
4
5
6
7
8
__(function() {
  try {
    var data = fs.readFile.sync(path)
    console.log(data)
  } catch (err) {
    console.log(err)
  } 
})

Note that no callback is passed to readFile. Instead readFile evaluates to its result or throws an Error.

Creating Fibers

In order to synchronously call asynchronous functions, the calls must execute inside of a Fiber.

This is done via the __ operator. The __ operator takes a function of zero arguments and executes that function inside of a Fiber. Calls to functions via sync inside the Fiber will cause the Fiber to block until the function’s callback is called, but will do so without blocking the process. Any IO performed inside those functions is still asynchronous.

Here is a full example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var fs = require('fs')

var carbon = require('@carbon-io/carbon-io')
var __ = carbon.fibers.__(module)

__(function() {
  var path = process.env.EXAMPLE_PATH || '/foo/bar'
  try {
    var data = fs.readFile.sync(path)
    console.log(data)
  } catch (err) {
    console.log(err)
  } 
})

Again notice that no callback is passed to readFile.sync.

The __ operator itself executes synchronously or asynchronously depending on the context in which it is invoked. If it is called within the context of an active Fiber, it executes synchronously, otherwise it executes asynchronously. This allows you to have multiple entry points to your application, bootstrapping as appropriate, while pulling in other Fiber aware modules synchronously. While the usefulness of this may not be immediately obvious, it should become clear as you become more familiar with the Carbon.io landscape (see: @carbon-io/test-tube).

To illustate, the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var fs = require('fs')

var carbon = require('@carbon-io/carbon-io')
var __ = carbon.fibers.__(module)

console.log('foo')

__(function() {                 // asynchronous
  
  console.log('---')

  __(function() {               // synchronous
    console.log(0)              
  })
  
  __(function() {               // synchronous
    return 1
  }, function(err, result) {
    console.log(result)
  })

  __(function() {               // synchronous
    console.log(2)
  })

  __(function() {               // synchronous
    console.log(3)
  })

  __(function() {               // synchronous
    console.log(4)
    return 5
  }, function(err, result) {
    console.log(result)
  })
})

console.log('bar')

__(function() {                 // asynchronous

  console.log('---')

  __.spawn(function() {         // asynchronous
    console.log(0)              
  })

  __(function() {               // synchronous
    return 1
  }, function(err, result) {
    console.log(result)
  })

  __.spawn(function() {         // asynchronous
    console.log(2)
  })

  __(function() {               // synchronous
    console.log(3)
  })

  __(function() {               // synchronous
    console.log(4)
    return 5
  }, function(err, result) {
    console.log(result)
  })
})

console.log('baz')

produces this output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
foo
bar
baz
---
0
1
2
3
4
5
---
1
3
4
5
0
2

In addition to the bare __ operator, as you may have noticed from the previous example, fibers provides the __.spawn function which will always execute asynchronously (__ actually delegates execution to __.spawn in the case that it is invoked outside of an active Fiber).

Using sync

The fibers module exposes a sync property on both Functions and Objects.

When sync is used on a function, sync returns a new function that is a synchronous version of the original. The new function takes the same arguments as the original, except the final callback parameter, and returns the result or throws an Error if an error occurs.

1
2
3
4
5
try {
  var data = fs.readFile.sync(path)
} catch (err) {
  console.log(err)
} 

When sync is used on an object, sync returns a new object that is the same as the original except all methods are synchronous.

1
2
3
4
5
try {
  var data = fs.sync.readFile(path)
} catch (err) {
  console.log(err)
} 

When working with instances of “classes” where methods may interact with a this, one should use the second form: obj.sync.<method>(...).

Return Values

Currently, __ and __.spawn return undefined. If you want to perform some computation asynchronously and retrieve the result, you can do this by passing an “errback”.

Using Fibers in Carbon.io applications

When using Fibers in Carbon.io applications you will want to make sure that all entry points to the event loop are wrapped in __.

One example of this is your main program. You will often see example applications structured like this:

1
2
3
__(function() {
  // application code goes here
})

This ensures that your application code is run in a Fiber, which will allow you to use sync in your application code.

Additionally, you will often see test modules structured as follows:

1
2
3
4
5
__(function() {
  module.exports = o({
    // application code and exported functionality
  })
})

This indicates that the module can be used as both an entry point and as a dependency. Dependants of modules written in this fashion should be careful to require these dependencies from within a call to __. If this pattern is not adhered to, strange behavior will result as module.exports will be initialized asynchronously and the dependant module will receive an empty object (e.g. {}) instead of the expected value.

Carbon.io Service objects automatically wrap each HTTP request in a Fiber so that your code that handles HTTP requests can use sync.

You may find that there are other times when you need your code to run in a Fiber, such as the callback to a timer. In each instance you can use the __ function to create and run a Fiber.

The application structure section of the server guide provides more information on how to use Fibers with Carbond.

Debugging

If your application is exiting suddenly or behaving differently than you would expect, it could be that an unhandled exception is the cause. Normally, these exceptions are swallowed silently (note, this is a programming error as these exceptions should be handled further up the call chain), but you can enable logging of these errors via the debug module by adding DEBUG="@carbon-io/fibers" to the environment.