-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add bind:true option to getCurrentContext #19
Add bind:true option to getCurrentContext #19
Conversation
Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
Can one of the admins verify this patch? |
1 similar comment
Can one of the admins verify this patch? |
server/middleware/per-request.js
Outdated
@@ -31,6 +31,16 @@ function perRequestContextFactory(options) { | |||
var scope = options.name || name; | |||
var enableHttpContext = options.enableHttpContext || false; | |||
var ns = LoopBackContext.createContext(scope); | |||
// Monkey-patch LoopBackContext.getCurrentContext in order to fix issue #17 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering - why are we applying this workaround only in the middleware, instead of binding get/set methods directly inside the original getCurrentContext
?
Also IIUC, this code is fixating the context
object to a single value when a request reaches per-request
middleware. How is this going to support the case when there are multiple requests handled in parallel?
Imagine the following handler chain (three different middleware handlers):
- per-context
- access-token retrieval, finishes in a later tick
- store access token in the context
Now consider two requests handled in the following sequence:
- Request1 invokes per-context, modifies global
LoopBackContext.getCurrentContext
to return CTX1 - Request1 starts access-token retrieval
- Request2 invokes per-context, modifies global
LoopBackContext.getCurrentContext
to return CTX2 - Request2 starts access-token retrieval
- Request1 access token is retrieved
- The access token is stored in CTX2. Ooops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to bind inside the original method, but for some reasons it didn't work.
It should work if I monkey-patch the method inside, say, a utility module. This will be my next attempt (less dirty than the first), unless I figure out why it doesn't work in the original method. Please wait for the commit.
I agree that it needs to be moved away from the middleware factory. In fact, if you add the middleware to the app, it works (even outside the middleware); but if you don't add the middleware to the app... whoops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the concurrent requests, this is done exactly for them. Even if the code needs to be fixed, tests pass with the workaround (and fail if I comment out the workaround), and it already works in a test app with 10 concurrent curl
requests.
I have a possible explanation. Are you sure that points 1 and 3 are correct? In these points, the function that gets invoked is this:
https://github.com/strongloop/loopback-context/blob/v1.0.0/server/middleware/per-request.js#L36
This function (the middleware) does not modify the global LoopBackContext.getCurrentContext (it's the outer function, i.e. the middleware factory, that does, and the latter gets called when adding the middleware to the app).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, now the workaround is outside the middleware [factory], and it still works (and this time, this is what I expected) in my test app with 10 concurrent curl
requests.
And tests pass only with the workaround in place. They fail as soon as you comment the workaround out. 👍
Also, I refactored the original method. I no longer monkey-patch it, which was not beautiful.
I bet it can be done without the proxy, but since I couldn't manage to do it, I'm fine with the proxy.
The proxy makes it clearer, I hope. I delegate the get
and set
methods of the cls namespace to a local object (boundMethods
) which gets created every time you call getCurrentContext()
(I used a new object so that the binding of this
when you call ctx.get
or ctx.set
is predictable, and can't get mixed up between requests).
And at the same time, I re-bind the methods to the (globally visible) namespace; more precisely, I bind them to the local variable in the body of createContext()
which holds a reference to the (globally visible) namespace.
test/main.test.js
Outdated
var timeout = 50; | ||
// Concurrent execution number 1 of 2 | ||
var execution1 = new Promise(function execution1(outerResolve, reject) { | ||
LoopBackContext.runInContext(function pushToContext1() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT, these tests are not invoking per-request
middleware, therefore they are not verifying the change proposed in that middleware :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, you are right, they are not invoking the middleware (I don't need it in the test; I only need parallel execution). The tests pass anyway (and a test app with concurrent requests works), after adding the workaround. But there is one more reason, that is, the middleware factory was called before, somewhere... I'll need to fix that. Tests should pass for only one reason (i.e. the addition of the fix).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Problem solved without changing the tests, since the change is no longer in the middleware [factory] (I moved it inside the original method, instead of patching it in the middleware [factory])
test/main.test.js
Outdated
@@ -129,4 +130,50 @@ describe('LoopBack Context', function() { | |||
], done); | |||
}); | |||
}); | |||
it('doesn\'t mix up contexts if using concurrently then() from when 3.7.7', | |||
function() { | |||
expect(require('when/package.json').version).to.equal('3.7.7'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am little bit worried about this long-distance coupling between the tests and package.json, and also what will happen when we will want to test two different versions of a same package.
Could we run npm install [email protected]
from the test instead? To speed up local npm test
and prevent repeated fetching of packages from the registry, we can use https://www.npmjs.com/package/strong-cached-install to cache the results - I have had good experience using this approach in other LoopBack projects. We will need to expose API for installing a single package, but that should be trivial - see the existing installPackage function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WIP
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL
Solved in a different way (local modules and stub modules)
Inspiration from here:
Local modules http://stackoverflow.com/a/26028854
Stub modules npm/npm#5499 (comment)
Works for me using two different, conflicting versions of the same package.
e9ba79a
to
db9387e
Compare
Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
Can one of the admins verify this patch? |
1 similar comment
Can one of the admins verify this patch? |
Sorry, this is a test comment to see if the pull requests gets re-opened and the disappeared commit re-appears... |
This issue also comes when we use |
@jainisenough Sorry, I made a mistake in issue #17. Fixed. The issue is not that the context gets lost, but rather mixed up between concurrent HTTP requests. Sorry if I confused you. |
35d541e
to
9f3dfca
Compare
I just wanted to write a reminder that this PR doesn't seem to fix strongloop/loopback#2519 (I suspect it's because this PR only works for user code, not 3rd-party code invoking "cls-unaware" libraries). |
@josieusa, No. this is context lost issue when we call third party api via loopback-context-rest. i'm aware about [email protected] issue, so I'm using 1.17.0 right now. |
@jainisenough the issue #17 is that the context gets mixed up between requests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @josieusa for the update, the patch looks much better now! I still have few concerns to discuss and/or address, see below.
@@ -26,13 +26,14 @@ | |||
"cls-hooked": "^4.0.1" | |||
}, | |||
"devDependencies": { | |||
"async": "1.5.2", | |||
"async-1.5.2": "file:./test/stub-modules/async-1.5.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice trick 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work on windows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using local paths for dependencies is a regular npm feature described in npm docs, see https://docs.npmjs.com/files/package.json#local-paths
I would expect it to work on Windows too.
server/current-context.js
Outdated
@@ -78,7 +78,28 @@ LoopBackContext.createContext = function(scopeName) { | |||
process.context[scopeName] = ns; | |||
// Set up LoopBackContext.getCurrentContext() | |||
LoopBackContext.getCurrentContext = function() { | |||
return ns && ns.active ? ns : null; | |||
if (ns && ns.active) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!ns || !ns.active)
return null;
// the rest of the code is indented one level less
server/current-context.js
Outdated
target[name]; | ||
}, | ||
}; | ||
return new Proxy(ns, handler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find using Proxy
rather heavy weight. How about using regular prototypal inheritance, would it not work equally well?
var boundContext = Object.create(ns);
boundContext.get = ns.bind(ns.get);
boundContext.set = ns.bind(ns.set);
return boundContext;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proxy
also seems to be not available in Node 4.x, see https://travis-ci.org/strongloop/loopback-context/jobs/189233795#L264
Unhandled error for request GET /TestModels/test:
ReferenceError: Proxy is not defined
at Object.LoopBackContext.getCurrentContext (/home/travis/build/strongloop/loopback-context/server/current-context.js:99:20)
test/main.test.js
Outdated
setTimeout(resolve, timeout); | ||
}); | ||
whenPromise.then(function pullFromContext2() { | ||
var testValue = ctx && ctx.get('test-key', 'test-value-2'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the second argument of get
ignored?
test/main.test.js
Outdated
function() { | ||
var timeout = 50; | ||
// Concurrent execution number 1 of 2 | ||
var execution1 = new Promise(function execution1(outerResolve, reject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering that the code building execution1 and execution2 differ only in one parameter (the value stored in the context), the code would be much easier to read if the creation of execution promise was refactored into a shared function.
Promise.all([
runTestWithValue('test-value-1'),
runTestWithValue('test-value-2'),
]);
function runTestWithValue(value) {
LoopBackContext.runInContext(...);
}
test/main.test.js
Outdated
expect(ctx).is.an('object'); | ||
ctx.set('test-key', 'test-value-1'); | ||
var whenPromise = whenV377.promise(function(resolve) { | ||
setTimeout(resolve, timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we simplify this code by using promise.delay
? See https://github.com/cujojs/when/blob/master/docs/api.md#promisedelay
test/main.test.js
Outdated
}).then(function verify1(testValue) { | ||
expect(testValue).to.equal('test-value-1'); | ||
outerResolve(); | ||
}).catch(function(error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.catch(reject)
server/current-context.js
Outdated
if (ns && ns.active) { | ||
/** | ||
* **NOTE** | ||
* This only re-binds get and set methods, the most used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am worried about breaking applications that expects getCurrentContext
to return a value that is not bound yet.
How about introducing a new API for getting the bound context? E.g. loopbackContext.getBoundContext
? Alternatively, we can add an options
object to getCurrentContext
, e.g. loopbackContext.getCurrentContext({ bind: true })
.
Thoughts?
@bajtos all done. I chose the |
Just one question: is |
I read the spec. Polyfills of forEach (not map) in Javascript code that follow the spec and use process.nextTick (so they are non-blocking) are possible. But I think that all native implementations are blocking. So it's ok to use forEach and return immediately after. |
Hello, |
Hi @josieusa, I was thinking about this problem for some time now and before I took a closer look at the code again, I have one concern I'd like to discuss with you. The solution proposed here "fixes" the lost context for the code inside a function that 1) started with the correct context 2) then called context-breaking code. However, it does not fix the lost context for any code that's called after this function. Here is an example to illustrate what I mean. It uses a middleware, but a similar example can be written using remote hooks, operation hooks or a remote method. app.use(function(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({ bind: true });
doSomethingThatBreaksContext(function(err) {
if (err) return next(err);
// cool, ctx.get() still works
next();
});
});
app.use(function(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({ bind: true });
// ouch, the context broken :(
}); I think the solution is to bind all callback invocation to the current context too, i.e. app.use(function(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({ bind: true });
next = ctx.bind(next); // <-- IMPORTANT!
doSomethingThatBreaksContext(function(err) {
if (err) return next(err);
// cool, ctx.get() still works
next();
});
});
app.use(function(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({ bind: true });
// cool, ctx is good
}); Using this principle, perhaps it is better to recommend people to bind all their callbacks, instead of binding the context methods? Using your var ctx = LoopBackContext.getCurrentContext();
expect(ctx).is.an('object');
ctx.set('test-key', pushedValue);
var whenPromise = whenV377().delay(timeout);
whenPromise.then(ctx.bind(function pullFromContextAndReturn() {
var pulledValue = ctx && ctx.get('test-key');
outerResolve({
pulledValue: pulledValue,
pushedValue: pushedValue,
});
})).catch(ctx.bind(reject));
}); That way the new Another idea to consider in case you prefer to keep the new app.use(function(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({ bind: true });
doSomethingThatBreaksContext(function(err) {
if (err) return ctx.bind(next)(err);
// cool, ctx.get() still works
ctx.bind(next)();
});
}); Thoughts? |
I prefer this, if it gives you the freedom to call |
Sounds good. Would you mind extending this pull request to bind |
sure! |
Hello,
That way, the first time that The second time that It's important to note that, in this example, this context instance is returned only for this request, and not for other concurrent requests. There is no mixing up. Problems:
EDIT maybe WeakMap is supported in Node 4, see http://node.green/ In The developer should delete the context from the map when the task is done. EDIT Another way, which would be easier to use, would be to attach a "good" context instance to EDIT 2 |
Regarding the solution that uses a WeakMap, in the case of operation hooks, since
Why not use |
Weird. Are you referring to my code snippets from #19 (comment)? They don't show the implementation in boundContext.bind = ns.bind(function(fn, context) {
return ns.bind.apply(ns, arguments);
});
I am afraid there is no such thing like a good or a bad context instance, at least as far as my understanding of CLS goes. The Also code that has access to TBH, I feel like we are over-engineering this. CLS will never work 100% reliably until it's fixed in Node.js core. We need to draw a line where adding extra reliability is not worth the required increase in code/usage complexity. In that light, I am proposing to land this patch as it is now, because it provides a nice small improvement that's worth it. Thoughts? |
That's exactly the case. At least for me. I'll try your new example instead, and let you know.
Yes, either in
When I said, "good instance", I meant that the issue was fixed by re-using the same instance. Of course that doesn't mean that the instance can't get broken in different ways; in fact it could (I didn't test the open connection pool issues yet, for example). The practical answer is to write tests on a case-by-case basis.
If we don't patch the "two middlewares" use case in your example, I feel that something is missing. At least some documentation about that case. I'll take a few days to keep thinking about it, while I try your new example. |
Sounds good. Take your time and let me know when you come up with something. |
Hello,
I'll take my time to understand the magic, make more tests, and then merge. |
Hello again,
😎😎😎😎😎😎😎😎 |
@bajtos PTAL |
This is an alternative snippet to othiym23/node-continuation-local-storage#29 (comment) The idea is the same. It uses |
- fix jsdoc for getCurrentContext - clean up getCurrentContext code - clean up the new test
Description
Workaround issue #17 with cujojs/when v3.7.7 and similar packages (mostly
Promise
libraries) that use queues for async-related stuff.User code is cleaner and more elegant than initially proposed in #17, because he/she only needs to remember to call
LoopBackContext.getCurrentContext()
at the first line of his/her middlewares/operation hooks which use the current context feature (if it's a remote hook, the feature should not be needed anymore; if you use it anyway, use it preferably inbeforeRemote
rather thanafterRemote
).P.S. : use it early in the middleware/hook chain for a given route.
Library code, instead, can be improved a lot; this is just my first attempt. Style needs to be checked too.
Also, I only patched the context functions
get
andset
, because I don't use other functions. I didn't patch the other functions yet (I deleted them, whoops 😄 This aspect needs to be fixed).Thank you!
Related issues
Checklist
guide