Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

WIP: add new continuation-local-storage module #5990

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/_toc.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Child Processes](child_process.html)
* [Cluster](cluster.html)
* [Console](console.html)
* [Continuation Local Storage](continuation_local_storage.html)
* [Crypto](crypto.html)
* [Debugger](debugger.html)
* [DNS](dns.html)
Expand Down
185 changes: 185 additions & 0 deletions doc/api/continuation_local_storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Continuation-Local Storage

Stability: 1 - Experimental

Continuation-local storage works like thread-local storage in threaded
programming, but is based on chains of Node-style callbacks instead of threads.
The standard Node convention of functions calling functions is very similar to
something called ["continuation-passing style"][cps] in functional programming,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: CPS is not restricted to functional languages (and I don't think it's widely used in FP outside of compilers.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until Node, I don't think that CPS was widely used anywhere besides as a mechanical transformation applied to FP source to turn it into imperative code. I can clarify in the doc, though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK OK OK so I forgot about Lisp. Jeez. Sorry, Twitter. Still want to draw a clear distinction between formal continuation-passing style (and "real" continuations, delimited or otherwise) and what Node does / what this thing does.

/cc @domenic @isaacs @nexxy @mikeal

and the name comes from the way this module allows you to set and get values
that are scoped to the lifetime of these chains of function calls.

Suppose you're writing a module that fetches a user and adds it to a session
before calling a function passed in by a user to continue execution:

```javascript
// setup.js

var createNamespace = require('continuation_local_storage').createNamespace;
var session = createNamespace('my session');

var db = require('./lib/db.js');

function start(options, next) {
db.fetchUserById(options.id, function (error, user) {
if (error) return next(error);

session.set('user', user);

next();
});
}
```

Later on in the process of turning that user's data into an HTML page, you call
another function (maybe defined in another module entirely) that wants to fetch
the value you set earlier:

```javascript
// send_response.js

var getNamespace = require('continuation_local_storage').getNamespace;
var session = getNamespace('my session');

var render = require('./lib/render.js')

function finish(response) {
var user = session.get('user');
render({user: user}).pipe(response);
}
```

When you set values in continuation-local storage, those values are accessible
until all functions called from the original function – synchronously or
asynchronously – have finished executing. This includes callbacks passed to
`process.nextTick` and the [timer functions][] ([setImmediate][],
[setTimeout][], and [setInterval][]), as well as callbacks passed to
asynchronous functions that call native functions (such as those exported from
the `fs`, `dns`, `zlib` and `crypto` modules).

A simple rule of thumb is anywhere where you might have set a property on the
`request` or `response` objects in an HTTP handler, you can (and should) now
use continuation-local storage. This API is designed to allow you extend the
scope of a variable across a sequence of function calls, but with values
specific to each sequence of calls.

Values are grouped into namespaces, created with `createNamespace()`. Sets of
function calls are grouped together by calling them within the function passed
to `.run()` on the namespace object. Calls to `.run()` can be nested, and each
nested context this creates has its own copy of the set of values from the
parent context. When a function is making multiple asynchronous calls, this
allows each child call to get, set, and pass along its own context without
overwriting the parent's.

A simple, annotated example of how this nesting behaves:

```javascript
var createNamespace = require('contination_local_storage').createNamespace;

var writer = createNamespace('writer');
writer.set('value', 0);

function requestHandler() {
writer.run(function(outer) {
// writer.get('value') returns 0
// outer.value is 0
writer.set('value', 1);
// writer.get('value') returns 1
// outer.value is 1
process.nextTick(function() {
// writer.get('value') returns 1
// outer.value is 1
writer.run(function(inner) {
// writer.get('value') returns 1
// outer.value is 1
// inner.value is 1
writer.set('value', 2);
// writer.get('value') returns 2
// outer.value is 1
// inner.value is 2
});
});
});

setTimeout(function() {
// runs with the default context, because nested contexts have ended
console.log(writer.get('value')); // prints 0
}, 1000);
}
```

## cls.createNamespace(name)

* return: {Namespace}

Each application wanting to use continuation-local values should create its own
namespace. Reading from (or, more significantly, writing to) namespaces that
don't belong to you is a faux pas.

## cls.getNamespace(name)

* return: {Namespace}

Look up an existing namespace.

## process.namespaces

* return: dictionary of {Namespace} objects

Continuation-local storage has a performance cost, and so it isn't enabled
until the module is loaded for the first time. Once the module is loaded, the
current set of namespaces is available in `process.namespaces`, so library code
that wants to use continuation-local storage only when it's active should test
for the existence of `process.namespaces`.

## Class: Namespace

Application-specific namespaces group values local to the set of functions
whose calls originate from a callback passed to `namespace.run()` or
`namespace.bind()`.

### namespace.active

* return: the currently active context on a namespace

### namespace.set(key, value)

* return: `value`

Set a value on the current continuation context.

### namespace.get(key)

* return: the requested value, or `undefined`

Look up a value on the current continuation context. Recursively searches from
the innermost to outermost nested continuation context for a value associated
with a given key.

### namespace.run(callback)

* return: the context associated with that callback

Create a new context on which values can be set or read. Run all the functions
that are called (either directly, or indirectly through asynchronous functions
that take callbacks themselves) from the provided callback within the scope of
that namespace. The new context is passed as an argument to the callback
whne it's called.

### namespace.bind(callback, [context])

* return: a callback wrapped up in a context closure

Bind a function to the specified namespace. Works analogously to
`Function.bind()` or `domain.bind()`. If context is omitted, it will default to
the currently active context in the namespace.

## context

A context is a plain object created using the enclosing context as its prototype.

[timer functions]: timers.html
[setImmediate]: timers.html#timers_setimmediate_callback_arg
[setTimeout]: timers.html#timers_settimeout_callback_delay_arg
[setInterval]: timers.html#timers_setinterval_callback_delay_arg
[cps]: http://en.wikipedia.org/wiki/Continuation-passing_style
148 changes: 148 additions & 0 deletions lib/continuation_local_storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var assert = require('assert');

var namespaces = Object.create(null);
Object.defineProperty(process,
'namespaces',
{
enumerable: true,
writable: false,
configurable: false,
value: namespaces
});

function each(obj, action) {
var keys = Object.keys(obj);
for (var i = 0, l = keys.length; i < l; ++i) {
var key = keys[i];
action(key, obj[key]);
}
}

function wrapContinuations(callback) {
// get the currently active contexts in all the namespaces.
var contexts = {};
each(namespaces, function(name, namespace) {
contexts[name] = namespace.active;
});

// return a callback that enters all the saved namespaces when called.
return function() {
each(contexts, function(name, context) {
namespaces[name].enter(context);
});
try {
return callback.apply(this, arguments);
} finally {
each(contexts, function(name, context) {
namespaces[name].exit(context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ISTM the exit callbacks should run in reverse order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: Never mind, got confused by the asserts in Namespace#exit().

});
}
};
}
Object.defineProperty(process,
'_wrapContinuations',
{
enumerable: true,
writable: false,
configurable: false,
value: wrapContinuations
});

function Namespace(name) {
namespaces[name] = this;

this.name = name;

// TODO: by default, contexts nest -- but domains won't
this._stack = [];

// every namespace has a default / "global" context
// FIXME: domains require different behavior to preserve distinction between
// _makeCallback and _makeDomainCallback, for performance reasons.
this.active = Object.create(null);
}

Namespace.prototype.set = function(key, value) {
this.active[key] = value;
return value;
};

Namespace.prototype.get = function(key) {
return this.active[key];
};

Namespace.prototype.createContext = function() {
return Object.create(this.active);
};

Namespace.prototype.run = function(fn) {
var context = this.createContext();
this.enter(context);
fn(context);
this.exit(context);
return context;
};

Namespace.prototype.bind = function(fn, context) {
if (!context)
context = this.active;

var self = this;
return function() {
self.enter(context);
var result = fn.apply(this, arguments);
self.exit(context);
return result;
};
};

Namespace.prototype.enter = function(context) {
assert(context, 'context must be provided for entering');
this._stack.push(this.active);
this.active = context;
};

// TODO: generalize nesting via configuration to handle domains
Namespace.prototype.exit = function(context) {
assert(context, 'context must be provided for exiting');

// Fast path for most exits that are at the top of the stack
if (this.active === context) {
assert(this._stack.length, 'can\'t remove top context');
this.active = this._stack.pop();
return;
}

// Fast search in the stack using lastIndexOf
var index = this._stack.lastIndexOf(context);
assert(index >= 0, 'context not currently entered; can\'t exit');
assert(index, 'can\'t remove top context');
this.active = this._stack[index - 1];
this._stack.length = index - 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When (and why) would context not be at the top of the stack?

Also, the "can't remove top context" error message is kind of confusing because it's actually at the bottom of the stack, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This chunk of the API is modeled directly on the same piece of domains -- because we expose enter and exit directly on the namespace, it's possible that people will enter the same context multiple times, and that enter and exit won't always be nested in the ways you expect. Also, the possibility of nonlocal exit (i.e. throws) means that the continuations need to be cleared up. This is demonstrated in the test cases on othiym23/node-continuation-local-storage-glue. I'll make sure all those test cases get ported over as I keep working on this thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should be "outermost" rather than "top", and I don't know how I feel about having so many JS asserts in Node core, especially in code that will be on a hot path when it's being used. Should I use macros? Test and throw? Not sure what the best pattern is here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never use assert in hot code. Calling to a global function requires a
context switch, but since we do the same with debug and the new until.is*
not we actually care.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trevnorris are you saying the code should switch from assert to util.*? It's not clear from your wording.

On Aug 5, 2013, at 8:50, Trevor Norris notifications@github.com wrote:

In lib/continuation_local_storage.js:

+Namespace.prototype.exit = function(context) {

  • assert(context, 'context must be provided for exiting');
  • // Fast path for most exits that are at the top of the stack
  • if (this.active === context) {
  • assert(this._stack.length, 'can't remove top context');
  • this.active = this._stack.pop();
  • return;
  • }
  • // Fast search in the stack using lastIndexOf
  • var index = this._stack.lastIndexOf(context);
  • assert(index >= 0, 'context not currently entered; can't exit');
  • assert(index, 'can't remove top context');
  • this.active = this._stack[index - 1];
  • this._stack.length = index - 1;
    Never use assert in hot code. Calling to a global function requires a context switch, but since we do the same with debug and the new until.is* not we actually care.

    Reply to this email directly or view it on GitHub.

};

module.exports = {
createNamespace: function(name) { return new Namespace(name); },
getNamespace: function(name) { return namespaces[name]; }
};
9 changes: 9 additions & 0 deletions lib/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ exports.setTimeout = function(callback, after) {

timer = new Timeout(after);

if (process._wrapContinuations)
callback = process._wrapContinuations(callback);

if (arguments.length <= 2) {
timer._onTimeout = callback;
} else {
Expand Down Expand Up @@ -233,6 +236,9 @@ exports.setInterval = function(callback, repeat) {
repeat = 1; // schedule on next tick, follows browser behaviour
}

if (process._wrapContinuations)
callback = process._wrapContinuations(callback);

var timer = new Timeout(repeat);
var args = Array.prototype.slice.call(arguments, 2);
timer._onTimeout = wrapper;
Expand Down Expand Up @@ -340,6 +346,9 @@ exports.setImmediate = function(callback) {

L.init(immediate);

if (process._wrapContinuations)
callback = process._wrapContinuations(callback);

immediate._onImmediate = callback;

if (arguments.length > 1) {
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'lib/child_process.js',
'lib/console.js',
'lib/constants.js',
'lib/continuation_local_storage.js',
'lib/crypto.js',
'lib/cluster.js',
'lib/dgram.js',
Expand Down
Loading