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

context: core module to manage generic contexts for async call chains #5243

Closed
othiym23 opened this issue Apr 8, 2013 · 20 comments
Closed
Milestone

Comments

@othiym23
Copy link

othiym23 commented Apr 8, 2013

The domains mechanism is a useful tool for adding context to errors raised in asynchronous call chains (or, if you like living dangerously / tempting the wrath of @isaacs, to recover from errors without restarting your services). It also almost serves the purposes of developers who want to annotate async call chains with metadata or extra state (examples: logging, tracing / profiling, generic instrumentation), but due to the needs of error-handling, it doesn't quite generalize enough to be truly useful in this regard. There are modules that allow developers to do similar things when they have full control over their stacks (CrabDude/trycatch and Gozala/reducers, among many others), but none of these modules are a good fit for developers writing tooling meant to be dropped transparently into user code.

See also #3733. /cc @isaacs

Here is a sketch at what the user-visible API might look like. My original attempt at this used a slightly modified version of the domains API with some special-purpose logic for dealing with nested contexts, but allowing multiple distinct namespaces is actually simpler and trades memory for execution time. It also makes it possible to special-case behavior for specific namespaces (i.e. my hope would be that domains would just become a specialized namespace, and _tickDomainCallback and _nextDomainTick would be all that would be required to deal with namespaces), although that isn't included here.

The API (context.js):

var util = require('util')
  , EventEmitter = require('events').EventEmitter
  ;

/* Not including the rest of the namespace management code in node.js / node.cc,
 * which is largely analogous to the way domains are implemented in 0.8 / 0.10.
 */
var namespaces = process.namespaces = Object.create(null);


function Context(namespace) {
  EventEmitter.call(this);
  this.namespace = namespace;
  this.bag = Object.create(null);
}
util.inherits(Context, EventEmitter);

Context.prototype.enter = function () { this.namespace.active = this; };
Context.prototype.exit = function () { delete this.namespace.active; };
// TODO: Context.prototype.bind = function () {};
// TODO: Context.prototype.add = function () {};
Context.prototype.end = function () { this.emit('end'); };
Context.prototype.set = function (key, value) { this.bag[key] = value; };
Context.prototype.get = function (key) { return this.bag[key]; };
Context.prototype.run = function (callback) {
  this.enter();
  callback.call(this);
  this.exit();
  this.end();
};


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

Namespace.prototype.createContext = function () {
  var context = new Context(this);
};


var context = module.exports = {
  // multiple namespaces for multiple purposes, e.g. domains
  createNamespace : function (name) { return new Namespace(name); },
  getNamespace : function (name) { return namespaces[name]; }
};

Here's an example of how the API might be used:

var context = require('context');

// multiple contexts in use
var tracer = context.createNamespace('tracer');

function Trace(harvester) {
  this.harvester = harvester;
}

Trace.prototype.runHandler = function (callback) {
  var trace = tracer.createContext();

  trace.on('end', function () {
    var transaction = trace.get('transaction');
    this.harvester.emit('finished', transaction);
  };

  trace.run(callback);
};

Trace.prototype.annotateState = function (name, value) {
  var active = tracer.active;
  active.set(name, value);
};
@othiym23
Copy link
Author

othiym23 commented Apr 8, 2013

Thinking about it a little more, another approach puts the accessors for the state on the namespace, rather than the context. This is actually the way I'd want to use contexts most of the time:

// everything not redefined here remains the same

Context.prototype._set = function (name, value) {
  this.bag[name] = value;
};

Context.prototype._get = function (name) {
  return this.bag[name];
};


function Namespace(name) {
  EventEmitter.call(this);
  this.name = name;
  namespaces[name] = this;
}
util.inherits(Namespace, EventEmitter);

Namespace.prototype.set = function (name, value) {
  if (!this.active) {
    return;
    // alternately: this.emit('error', "No active context on namespace " + this.name + " to set " + name "."); return;
    // alternately: throw new Error("No active context on namespace " + this.name + " to set " + name ".");
  }

 this.active._set(name, value);
};

Namespace.prototype.get = function (name) {
  if (!this.active) {
    return;
    // alternately: this.emit('error', "No active context on namespace " + this.name + " to get " + name "."); return;
    // alternately: throw new Error("No active context on namespace " + this.name + " to get " + name ".");
  }

  return this.active._get(name);
};

This makes more sense for my use cases, but I'm on the fence as to whether it's the most appropriate general solution. This also allows for fallthrough lookup with nested contexts on a domain while decoupling the context lifecyle from managing the associated namespace state, which makes a lot of sense to me, but might not to everyone else.

@othiym23
Copy link
Author

https://github.com/othiym23/node-local-context contains an implementation of the above API. Another module will be along presently to dynamically patch the Node runtime to use this as a basis for domains as a proof of concept that domains will work with this style of API, as well as how it might work in Node core.

@ngrilly
Copy link

ngrilly commented May 12, 2013

I just had a look to your userland implementation of local context. It could very useful! One question: Do the programmer need to explicitly bind each callback to the current context, or is it implicit like with implicit domain binding"?

@othiym23
Copy link
Author

If you're using this module on its own, you'll need to explicitly bind functions into contexts. node-local-context is intended as a simple, standalone implementation of the strawman API.

I'm working right now on a separate module that will do implicit binding (by supplanting the built-in domain code in Node <= 0.10.x). It's definitely the trickier part of the implementation, but I hope to have that up and available shortly, and will link to it from this issue.

@ngrilly
Copy link

ngrilly commented May 13, 2013

That's very clear. Thanks for the clarification!

Regarding the "separate module that will do implicit binding", I have two questions:

1/ How would you summarize what is missing from the built-in domain module that makes necessary to create a new system to manage local context? In other words, what are the differences between domains and the new "local context" module? From other discussions I read on this topic, I understand the way domains handle errors make them inadequate to manage local contexts.

2/ When using a database connection pool, it's frequent for a connection to be created in the context of a domain attached to the current HTTP request. This is problematic when the connection is retrieved some time later in the context of another HTTP request, because then the database connection is incorrectly associated to the "old" close HTTP request, instead of the current one. The solution is of course to have some code to prevent that in the connection pool, or wrapping the connection pool, that makes sure to attach the database connection to the active domain just after poping the connection from the pool and before using it. My question: do you expect your new module to change or facilitate this in some way?

Anyway, thanks for your work on this topic!

@othiym23
Copy link
Author

How would you summarize what is missing from the built-in domain module that makes necessary to create a new system to manage local context? In other words, what are the differences between domains and the new "local context" module?

There are two primary differences:

  1. When a domain receives an error, it exits both its own domain and any nested domains. This mirrors the behavior of exceptions in JavaScript, which is actually a restricted form of a computed goto. I'm still feeling around the proper behavior for this, but my feeling is that I'll end up exposing a way to pass a callback to the namespace initializer that customizes the nesting behavior.
  2. For my use case, I need both domains and another, more generic namespace scoped to individual callback chains. It turns out that trying to override domains to support both is at best a clumsy conflation of concerns, at worst slow and unworkable. local-context is both more general and supports multiple namespaces. The tricky part of the module I'm working on right now is ensuring that all of the implicit contexts get propagated along with asynchronous execution.

My question: do you expect your new module to change or facilitate this in some way?

The use case you describe is the motivation for there being different methods for domain.run(), domain.add(), and domain.bind(). In general, it's impossible for Node to know your intentions when dealing with socket pools and other long-running connections. Even with modules like generic-pool and redis, the best way to ensure that user code is bound to the correct domain is to use domain.bind() to bind callbacks bound to calls on a specific Socket to the correct domain.

If you can come up with a description of a method to automate that, I'm open to trying to make it work with local-context or the new module. In general, the consensus of those of us who have been working with domains for a while is that this is a known corner case that needs to be worked around the way I describe above.

@SLaks
Copy link

SLaks commented May 14, 2013

@othiym23 Will you be creating a version of this API backed by domains to use with existing (node 0.8 & 0.10) code?

If not, I will probably create it this or next week. (for LogUp)

@othiym23
Copy link
Author

@SLaks That's exactly what I'm working on right now, but if you want to give it a shot, do! It's trickier than it looks!

@othiym23
Copy link
Author

Hey @SLaks, I've been working on this for about a week now, but monkeypatching stuff close to the event loop is actually weird, hard and scary. Have you poked at this at all? Do you want to join sources on this problem? I'm a strong enough nerd to admit when I'm up against the wall a little. 😉

@SLaks
Copy link

SLaks commented May 24, 2013

I've been busy with other stuff and have not had time to look at this recently.

What kind of problems are you encountering?
I had assumed this would be a simple matter of attaching properties to the current domain.

@othiym23
Copy link
Author

This actually supplants domains, not adds things to them -- without doing that, it's impossible to get the proper nesting behavior (also, with namespaces, domains become a namespace, rather than containing the namespaces). Right now, I'm experimenting with completely replacing all of the the JS side of the Node event loop, although I'm hoping to come up with something less invasive before I'm done.

I completely understand not having time, and I'll try to throw up what I have so far at the end of the day so others can see it. I just thought I'd throw myself on the mercy of the community, because this is proving to be kind of a pain in the ass. 🎉

@tjfontaine
Copy link

@othiym23 how much of this is covered by AL?

@othiym23
Copy link
Author

Between CLS, AL, and the pending tracing module, all of this is covered. Huge thanks to you, @trevnorris, @creationix, and the many others who helped with this!

@OrangeDog
Copy link

@othiym23 do we still use continuation-local-storage from npm, or is there another module, or example for how do do it in core?

@othiym23
Copy link
Author

@OrangeDog CLS is still working (and supported, although I don't have as much time for it as I'd like), you can take a look at StrongLoop's zone for an alternative approach that is a little more featureful than CLS, and there may be some chunks of the tracing API in io.js (in the tracing module).

@shrimpy
Copy link

shrimpy commented Oct 17, 2016

sorry for comment on this old thread.
So is this feature now build-in to nodejs v0.13 and above?

@OrangeDog
Copy link

There never was a v0.13

@shrimpy
Copy link

shrimpy commented Oct 17, 2016

thx @OrangeDog for pointing out. so does it mean this feature never make it into nodejs? or it is part of nodejs 4.x and above?

@mocheng
Copy link

mocheng commented Nov 21, 2016

Node v4 and v6 doesn't have this feature. So, this feature is abandoned?

@othiym23
Copy link
Author

othiym23 commented Nov 21, 2016

This turned into continuation-local-storage, built atop a set of low-level hooks that were partially included in Node 0.12 called async-listener (which was also shipped as a polyfill).

Since then, work has proceeded on the low-level hooks under the name async_wrap (which is based on a C++ base class that anything else that can create an asynchronous context gap that needs to be bridged) and now is under development as async_hooks (that's a long thread; budget some time to get up to speed).

It turns out it's tricky to implement something like this without complicated consequences for the overall performance of Node as a platform, and coming up with an implementation that has good performance while also handling the core use cases of performance monitoring and continuation-local storage has been a challenge. But it's work that's still in progress!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants