From ca0208ddd936a8573f1af6eb18edaff329fd4571 Mon Sep 17 00:00:00 2001 From: Pham Anh Tuan Date: Mon, 1 Dec 2014 17:36:34 +0700 Subject: [PATCH] Fix context middleware to preserve domains When executing a request using a pooled connection, connectors like MongoDB and/or MySQL rebind callbacks to the domain which issued the request, as opposed to the domain which opened the pooled connection. This commit fixes the context middleware to play nicely with that mechanism and preserve domain rebinds. --- server/middleware/context.js | 26 ++++++++++--- test/loopback.test.js | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/server/middleware/context.js b/server/middleware/context.js index c86903a0..23912db4 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -2,6 +2,7 @@ var loopback = require('../../lib/loopback'); var juggler = require('loopback-datasource-juggler'); var remoting = require('strong-remoting'); var cls = require('continuation-local-storage'); +var domain = require('domain'); module.exports = context; @@ -44,6 +45,13 @@ function context(options) { var scope = options.name || name; var enableHttpContext = options.enableHttpContext || false; var ns = createContext(scope); + + var currentDomain = process.domain = domain.create(); + currentDomain.oldBind = currentDomain.bind; + currentDomain.bind = function(callback, context) { + return currentDomain.oldBind(ns.bind(callback, context), context); + }; + // Return the middleware return function contextHandler(req, res, next) { if (req.loopbackContext) { @@ -53,13 +61,19 @@ function context(options) { // Bind req/res event emitters to the given namespace ns.bindEmitter(req); ns.bindEmitter(res); + + currentDomain.add(req); + currentDomain.add(res); + // Create namespace for the request context - ns.run(function processRequestInContext(context) { - // Run the code in the context of the namespace - if (enableHttpContext) { - ns.set('http', {req: req, res: res}); // Set up the transport context - } - next(); + currentDomain.run(function() { + ns.run(function processRequestInContext(context) { + // Run the code in the context of the namespace + if (enableHttpContext) { + ns.set('http', {req: req, res: res}); // Set up the transport context + } + next(); + }); }); }; } diff --git a/test/loopback.test.js b/test/loopback.test.js index 8f217f6a..26c4f22f 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -1,4 +1,7 @@ var it = require('./util/it'); +var describe = require('./util/describe'); +var Domain = require('domain'); +var EventEmitter = require('events').EventEmitter; describe('loopback', function() { var nameCounter = 0; @@ -388,4 +391,72 @@ describe('loopback', function() { }); }); }); + + describe.onServer('loopback.getCurrentContext', function() { + var runInOtherDomain; + var runnerInterval; + + before(function setupRunInOtherDomain() { + var emitterInOtherDomain = new EventEmitter(); + Domain.create().add(emitterInOtherDomain); + + runInOtherDomain = function(fn) { + emitterInOtherDomain.once('run', fn); + }; + + runnerInterval = setInterval(function() { + emitterInOtherDomain.emit('run'); + }, 10); + }); + + after(function tearDownRunInOtherDomain() { + clearInterval(runnerInterval); + }); + + // See the following two items for more details: + // https://github.com/strongloop/loopback/issues/809 + // https://github.com/strongloop/loopback/pull/337#issuecomment-61680577 + it('preserves callback domain', function(done) { + var app = loopback(); + app.use(loopback.rest()); + app.dataSource('db', { connector: 'memory' }); + + var TestModel = loopback.createModel({ name: 'TestModel' }); + app.model(TestModel, { dataSource: 'db', public: true }); + + // function for remote method + TestModel.test = function(inst, cb) { + var tmpCtx = loopback.getCurrentContext(); + if (tmpCtx) tmpCtx.set('data', 'a value stored in context'); + if (process.domain) cb = process.domain.bind(cb); // IMPORTANT + runInOtherDomain(cb); + }; + + // remote method + TestModel.remoteMethod('test', { + accepts: { arg: 'inst', type: uniqueModelName }, + returns: { root: true }, + http: { path: '/test', verb: 'get' } + }); + + // after remote hook + TestModel.afterRemote('**', function(ctxx, inst, next) { + var tmpCtx = loopback.getCurrentContext(); + if (tmpCtx) { + ctxx.result.data = tmpCtx.get('data'); + }else { + ctxx.result.data = 'context not available'; + } + next(); + }); + + request(app) + .get('/TestModels/test') + .end(function(err, res) { + if (err) return done(err); + expect(res.body.data).to.equal('a value stored in context'); + done(); + }); + }); + }); });