478 lines
18 KiB
JavaScript
478 lines
18 KiB
JavaScript
/* eslint-disable max-len */
|
|
'use strict';
|
|
|
|
const util = require('util');
|
|
const assert = require('assert');
|
|
const wrapEmitter = require('emitter-listener');
|
|
const async_hooks = require('async_hooks');
|
|
|
|
const CONTEXTS_SYMBOL = 'cls@contexts';
|
|
const ERROR_SYMBOL = 'error@context';
|
|
|
|
const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED;
|
|
|
|
let currentUid = -1;
|
|
|
|
module.exports = {
|
|
getNamespace: getNamespace,
|
|
createNamespace: createNamespace,
|
|
destroyNamespace: destroyNamespace,
|
|
reset: reset,
|
|
ERROR_SYMBOL: ERROR_SYMBOL
|
|
};
|
|
|
|
function Namespace(name) {
|
|
this.name = name;
|
|
// changed in 2.7: no default context
|
|
this.active = null;
|
|
this._set = [];
|
|
this.id = null;
|
|
this._contexts = new Map();
|
|
this._indent = 0;
|
|
}
|
|
|
|
Namespace.prototype.set = function set(key, value) {
|
|
if (!this.active) {
|
|
throw new Error('No context available. ns.run() or ns.bind() must be called first.');
|
|
}
|
|
|
|
this.active[key] = value;
|
|
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(indentStr + 'CONTEXT-SET KEY:' + key + '=' + value + ' in ns:' + this.name + ' currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true}));
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
Namespace.prototype.get = function get(key) {
|
|
if (!this.active) {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const asyncHooksCurrentId = async_hooks.currentId();
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
//debug2(indentStr + 'CONTEXT-GETTING KEY NO ACTIVE NS:' + key + '=undefined' + ' (' + this.name + ') currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true}));
|
|
debug2(`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}`);
|
|
}
|
|
return undefined;
|
|
}
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(indentStr + 'CONTEXT-GETTING KEY:' + key + '=' + this.active[key] + ' (' + this.name + ') currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true}));
|
|
debug2(`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${this.active[key]} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length} active:${util.inspect(this.active)}`);
|
|
}
|
|
return this.active[key];
|
|
};
|
|
|
|
Namespace.prototype.createContext = function createContext() {
|
|
// Prototype inherit existing context if created a new child context within existing context.
|
|
let context = Object.create(this.active ? this.active : Object.prototype);
|
|
context._ns_name = this.name;
|
|
context.id = currentUid;
|
|
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(`${indentStr}CONTEXT-CREATED Context: (${this.name}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length} context:${util.inspect(context, {showHidden:true, depth:2, colors:true})}`);
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
Namespace.prototype.run = function run(fn) {
|
|
let context = this.createContext();
|
|
this.enter(context);
|
|
|
|
try {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(`${indentStr}CONTEXT-RUN BEGIN: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} context:${util.inspect(context)}`);
|
|
}
|
|
fn(context);
|
|
return context;
|
|
} catch (exception) {
|
|
if (exception) {
|
|
exception[ERROR_SYMBOL] = context;
|
|
}
|
|
throw exception;
|
|
} finally {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(`${indentStr}CONTEXT-RUN END: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`);
|
|
}
|
|
this.exit(context);
|
|
}
|
|
};
|
|
|
|
Namespace.prototype.runAndReturn = function runAndReturn(fn) {
|
|
let value;
|
|
this.run(function (context) {
|
|
value = fn(context);
|
|
});
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* Uses global Promise and assumes Promise is cls friendly or wrapped already.
|
|
* @param {function} fn
|
|
* @returns {*}
|
|
*/
|
|
Namespace.prototype.runPromise = function runPromise(fn) {
|
|
let context = this.createContext();
|
|
this.enter(context);
|
|
|
|
let promise = fn(context);
|
|
if (!promise || !promise.then || !promise.catch) {
|
|
throw new Error('fn must return a promise.');
|
|
}
|
|
|
|
if (DEBUG_CLS_HOOKED) {
|
|
debug2('CONTEXT-runPromise BEFORE: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context));
|
|
}
|
|
|
|
return promise
|
|
.then(result => {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
debug2('CONTEXT-runPromise AFTER then: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context));
|
|
}
|
|
this.exit(context);
|
|
return result;
|
|
})
|
|
.catch(err => {
|
|
err[ERROR_SYMBOL] = context;
|
|
if (DEBUG_CLS_HOOKED) {
|
|
debug2('CONTEXT-runPromise AFTER catch: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context));
|
|
}
|
|
this.exit(context);
|
|
throw err;
|
|
});
|
|
};
|
|
|
|
Namespace.prototype.bind = function bindFactory(fn, context) {
|
|
if (!context) {
|
|
if (!this.active) {
|
|
context = this.createContext();
|
|
} else {
|
|
context = this.active;
|
|
}
|
|
}
|
|
|
|
let self = this;
|
|
return function clsBind() {
|
|
self.enter(context);
|
|
try {
|
|
return fn.apply(this, arguments);
|
|
} catch (exception) {
|
|
if (exception) {
|
|
exception[ERROR_SYMBOL] = context;
|
|
}
|
|
throw exception;
|
|
} finally {
|
|
self.exit(context);
|
|
}
|
|
};
|
|
};
|
|
|
|
Namespace.prototype.enter = function enter(context) {
|
|
assert.ok(context, 'context must be provided for entering');
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(`${indentStr}CONTEXT-ENTER: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`);
|
|
}
|
|
|
|
this._set.push(this.active);
|
|
this.active = context;
|
|
};
|
|
|
|
Namespace.prototype.exit = function exit(context) {
|
|
assert.ok(context, 'context must be provided for exiting');
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const asyncHooksCurrentId = async_hooks.executionAsyncId();
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent);
|
|
debug2(`${indentStr}CONTEXT-EXIT: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`);
|
|
}
|
|
|
|
// Fast path for most exits that are at the top of the stack
|
|
if (this.active === context) {
|
|
assert.ok(this._set.length, 'can\'t remove top context');
|
|
this.active = this._set.pop();
|
|
return;
|
|
}
|
|
|
|
// Fast search in the stack using lastIndexOf
|
|
let index = this._set.lastIndexOf(context);
|
|
|
|
if (index < 0) {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
debug2('??ERROR?? context exiting but not entered - ignoring: ' + util.inspect(context));
|
|
}
|
|
assert.ok(index >= 0, 'context not currently entered; can\'t exit. \n' + util.inspect(this) + '\n' + util.inspect(context));
|
|
} else {
|
|
assert.ok(index, 'can\'t remove top context');
|
|
this._set.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
Namespace.prototype.bindEmitter = function bindEmitter(emitter) {
|
|
assert.ok(emitter.on && emitter.addListener && emitter.emit, 'can only bind real EEs');
|
|
|
|
let namespace = this;
|
|
let thisSymbol = 'context@' + this.name;
|
|
|
|
// Capture the context active at the time the emitter is bound.
|
|
function attach(listener) {
|
|
if (!listener) {
|
|
return;
|
|
}
|
|
if (!listener[CONTEXTS_SYMBOL]) {
|
|
listener[CONTEXTS_SYMBOL] = Object.create(null);
|
|
}
|
|
|
|
listener[CONTEXTS_SYMBOL][thisSymbol] = {
|
|
namespace: namespace,
|
|
context: namespace.active
|
|
};
|
|
}
|
|
|
|
// At emit time, bind the listener within the correct context.
|
|
function bind(unwrapped) {
|
|
if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) {
|
|
return unwrapped;
|
|
}
|
|
|
|
let wrapped = unwrapped;
|
|
let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL];
|
|
Object.keys(unwrappedContexts).forEach(function (name) {
|
|
let thunk = unwrappedContexts[name];
|
|
wrapped = thunk.namespace.bind(wrapped, thunk.context);
|
|
});
|
|
return wrapped;
|
|
}
|
|
|
|
wrapEmitter(emitter, attach, bind);
|
|
};
|
|
|
|
/**
|
|
* If an error comes out of a namespace, it will have a context attached to it.
|
|
* This function knows how to find it.
|
|
*
|
|
* @param {Error} exception Possibly annotated error.
|
|
*/
|
|
Namespace.prototype.fromException = function fromException(exception) {
|
|
return exception[ERROR_SYMBOL];
|
|
};
|
|
|
|
function getNamespace(name) {
|
|
return process.namespaces[name];
|
|
}
|
|
|
|
function createNamespace(name) {
|
|
assert.ok(name, 'namespace must be given a name.');
|
|
|
|
if (DEBUG_CLS_HOOKED) {
|
|
debug2(`NS-CREATING NAMESPACE (${name})`);
|
|
}
|
|
let namespace = new Namespace(name);
|
|
namespace.id = currentUid;
|
|
|
|
const hook = async_hooks.createHook({
|
|
init(asyncId, type, triggerId, resource) {
|
|
currentUid = async_hooks.executionAsyncId();
|
|
|
|
//CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec
|
|
// let initContext = namespace.active;
|
|
// if(!initContext && triggerId) {
|
|
// let parentContext = namespace._contexts.get(triggerId);
|
|
// if (parentContext) {
|
|
// namespace.active = parentContext;
|
|
// namespace._contexts.set(currentUid, parentContext);
|
|
// if (DEBUG_CLS_HOOKED) {
|
|
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
// debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|
// }
|
|
// } else if (DEBUG_CLS_HOOKED) {
|
|
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
// debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|
// }
|
|
// }else {
|
|
// namespace._contexts.set(currentUid, namespace.active);
|
|
// if (DEBUG_CLS_HOOKED) {
|
|
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
// debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|
// }
|
|
// }
|
|
if(namespace.active) {
|
|
namespace._contexts.set(asyncId, namespace.active);
|
|
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} resource:${resource}`);
|
|
}
|
|
}else if(currentUid === 0){
|
|
// CurrentId will be 0 when triggered from C++. Promise events
|
|
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const triggerIdContext = namespace._contexts.get(triggerId);
|
|
if (triggerIdContext) {
|
|
namespace._contexts.set(asyncId, triggerIdContext);
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, { showHidden: true, depth: 2, colors: true })} resource:${resource}`);
|
|
}
|
|
} else if (DEBUG_CLS_HOOKED) {
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, { showHidden: true, depth: 2, colors: true })} resource:${resource}`);
|
|
}
|
|
}
|
|
|
|
|
|
if(DEBUG_CLS_HOOKED && type === 'PROMISE'){
|
|
debug2(util.inspect(resource, {showHidden: true}));
|
|
const parentId = resource.parentId;
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} resource:${resource}`);
|
|
}
|
|
|
|
},
|
|
before(asyncId) {
|
|
currentUid = async_hooks.executionAsyncId();
|
|
let context;
|
|
|
|
/*
|
|
if(currentUid === 0){
|
|
// CurrentId will be 0 when triggered from C++. Promise events
|
|
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|
//const triggerId = async_hooks.triggerAsyncId();
|
|
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|
}else{
|
|
context = namespace._contexts.get(currentUid);
|
|
}
|
|
*/
|
|
|
|
//HACK to work with promises until they are fixed in node > 8.1.1
|
|
context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid);
|
|
|
|
if (context) {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`);
|
|
namespace._indent += 2;
|
|
}
|
|
|
|
namespace.enter(context);
|
|
|
|
} else if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} namespace._contexts:${util.inspect(namespace._contexts, {showHidden:true, depth:2, colors:true})}`);
|
|
namespace._indent += 2;
|
|
}
|
|
},
|
|
after(asyncId) {
|
|
currentUid = async_hooks.executionAsyncId();
|
|
let context; // = namespace._contexts.get(currentUid);
|
|
/*
|
|
if(currentUid === 0){
|
|
// CurrentId will be 0 when triggered from C++. Promise events
|
|
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|
//const triggerId = async_hooks.triggerAsyncId();
|
|
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|
}else{
|
|
context = namespace._contexts.get(currentUid);
|
|
}
|
|
*/
|
|
//HACK to work with promises until they are fixed in node > 8.1.1
|
|
context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid);
|
|
|
|
if (context) {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
namespace._indent -= 2;
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`);
|
|
}
|
|
|
|
namespace.exit(context);
|
|
|
|
} else if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
namespace._indent -= 2;
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`);
|
|
}
|
|
},
|
|
destroy(asyncId) {
|
|
currentUid = async_hooks.executionAsyncId();
|
|
if (DEBUG_CLS_HOOKED) {
|
|
const triggerId = async_hooks.triggerAsyncId();
|
|
const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|
debug2(`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(namespace._contexts.get(currentUid))}`);
|
|
}
|
|
|
|
namespace._contexts.delete(asyncId);
|
|
}
|
|
});
|
|
|
|
hook.enable();
|
|
|
|
process.namespaces[name] = namespace;
|
|
return namespace;
|
|
}
|
|
|
|
function destroyNamespace(name) {
|
|
let namespace = getNamespace(name);
|
|
|
|
assert.ok(namespace, 'can\'t delete nonexistent namespace! "' + name + '"');
|
|
assert.ok(namespace.id, 'don\'t assign to process.namespaces directly! ' + util.inspect(namespace));
|
|
|
|
process.namespaces[name] = null;
|
|
}
|
|
|
|
function reset() {
|
|
// must unregister async listeners
|
|
if (process.namespaces) {
|
|
Object.keys(process.namespaces).forEach(function (name) {
|
|
destroyNamespace(name);
|
|
});
|
|
}
|
|
process.namespaces = Object.create(null);
|
|
}
|
|
|
|
process.namespaces = {};
|
|
|
|
//const fs = require('fs');
|
|
function debug2(...args) {
|
|
if (DEBUG_CLS_HOOKED) {
|
|
//fs.writeSync(1, `${util.format(...args)}\n`);
|
|
process._rawDebug(`${util.format(...args)}`);
|
|
}
|
|
}
|
|
|
|
/*function getFunctionName(fn) {
|
|
if (!fn) {
|
|
return fn;
|
|
}
|
|
if (typeof fn === 'function') {
|
|
if (fn.name) {
|
|
return fn.name;
|
|
}
|
|
return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1];
|
|
} else if (fn.constructor && fn.constructor.name) {
|
|
return fn.constructor.name;
|
|
}
|
|
}*/
|
|
|
|
|