-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Fixed nasty callback duplication. #535
Conversation
I had some case where the second error response was called during the callback of the first error response in an async call before the callback could be replaced with a noop. That resulted in the error case where the callback was called twice with an error message. That was pretty hard to debug. I figured that the same error could occur at other places as well and changed it there too. It would be nice if someone could provide unit tests for these changes, else I will provide them as soon as I have time for it.
I'd really want a failing test case (one that fails without your patch) before we merge this. It feels like you might be doing something wrong. |
It is pretty straightforward and harmless, and would catch some of the problems caused by calling-back multiple times. It won't catch everything though, because once you pass a callback to an iterator internally, you can't retroactively change it to a no-op. It does sort of enable improper async style -- you're only supposed to ever callback once. Always If a novice to Node wrote something like:
...they may wonder why |
Rather than making it more permissive, perhaps async needs a "strict" mode for development/testing/debugging where all callbacks provided to user code are automatically wrapped in the equivalent of Making async more permissive would simply delay discovering such things until they are much more expensive to find and fix. Introducing a mode where async is super strict would be an aid to learning for novices and would make such bugs much cheaper to find for pros. In cases such as the OP here, you guys could say "turn on |
@vsivsi let's not make it a mode, let's just make it more strict ;) @aearly how about we implement the early re-assignment of the callback as in this pull request, but instead of making it a no-op we replace it with a function which throws with a more informative error message? "Callback called more than once"... ? |
Let me provide with my error scenario: This is a rough sketch of the way this error bugged me: https://gist.github.com/martinheidegger/f962d8226d1e4a797f4e In my case the problem was a lot more tricky because it happened when an exceptions was thrown as a result of the callback triggered. That exception closed the node-domain which triggered the error event and upon the error event it all the remaining connections were closed, resulting in the callback triggering the other methods. Really shitty to debug. |
@caolan I was going to suggest that. People might complain about writing to stdout, but it will help catch legitimate errors in async control flow. @martinheidegger Your example is a bit hard to follow, but i think one of the problems is that |
I could get behind that! Also, I just realized that I had a mistaken view of what onceOrDie = (callback) ->
if async.strict
alreadyInvoked = false
return (params...) ->
if alreadyInvoked
throw new Error "Single use callback invoked multiple times"
else
alreadyInvoked = true
callback params...
else
return callback That's what I meant by So to put this back in the context of this PR: if functionality is changed to make async more permissive (which is how I read this PR) then it becomes more valuable to have a way to turn that off, so that when "you're using it wrong," it becomes blatantly obvious. However, as @caolan suggests, perhaps the right answer is just to make async stricter overall; which argues against the approach taken by this PR, IMO. |
There already is an Perhaps we just need to use this everywhere it applies. |
@aearly I changed my example to use clearTimeout but it results in the same error: https://gist.github.com/martinheidegger/c5a045d0e5ecd8f01e54 I also added some traces to illustrate the problem: https://gist.github.com/martinheidegger/d4661981a3c7c9b1529b Output:
The contract of each of requests (a) and (b) is by itself correct. Each callback is called only once. The problem is that the b callback is called before the async callback is finished. (Note: This example works if I add a nextTick in before interrupting all processes: https://gist.github.com/martinheidegger/4b5a3d25afced38b1b21) @ALL: I am against making this part "stricter" because yes: this problem could theoretically be solved in my case using a nextTick or setTimeout. However: when you run into this problem it can take a lot of time figuring out why there is a second call on async in the first place. This accidental-synchronous call can be a part of a system error in a second library you use that you can't easily change. |
@martinheidegger My issue with making libs like async be automatically more forgiving in such cases is that it has strong potential to silently mask the impact of more serious problems. If async was stricter overall, you'd find out about this kind of situation much sooner. If a more permissive solution turns out to be the best choice for your code, you can always use a As an aside, I often find that if I'm struggling too hard to make a bit of my own code "just work" with a fairly mature tool like async, I take it as a sign that it's time to step back and reconsider my own logic and/or my choice of tool. 80% of the time I find it's because I've picked the wrong tool for the job, or I'm going about it the wrong way. Not saying that's what's going on with your code, but worth considering. |
@vsivsi Async is already forgiving that both requests trigger a "callback" with an error. If you remove the interrupt trigger or move it to a nextTick (out of this method execution scope) then it will work just fine. It seems to me that this PR has nothing to do with the strictness of async but with consistency within the current strictness. It is correct usage for multiple calls to the callback with a error asynchronously. Why shouldn't it be correct to do the same thing synchronously. |
I feel the presence of Zalgo... |
Zalgo is with us and in us and makes us climb the walls always. |
@martinheidegger Is it fair to say that this is the simplest "failing case" that you are trying to fix? var async = require('async');
var shared = null;
async.parallel([
function(cb) {
return shared = function() {
shared = function() {};
return cb(new Error("Oh, hello!"));
};
}, function(cb) {
return cb(new Error("Bye bye"));
}
], function(err) {
console.log("" + err);
return shared();
}); Outputs:
|
Yes, looks like it. |
If I introduced logic like this into a program, I'd want to know about it as soon as possible. By my suggestion above, async would throw an Error instead of invoking the final callback a second time. If I understand it correctly, this PR would obscure this situation by simply eliminating the second invocation of the user callback. I'm bending my mind trying to imagine a situation where this is a good thing to allow... Looking at the code in your Gist, it seems to me that the source of the issue is that Fixing it this way requires no use of In my mind, this is a great example of why async should throw an Error in such cases rather than enabling them, so that what is really happening is revealed and logical errors can be caught and fixed, rather than obscured to manifest later in ever more insidious forms. Th͏e Da҉rk Pońy Lo͘r͠d HE ́C͡OM̴E̸S! |
@vsivsi The example I gave was pretty simplified, in reality it was more complex. For the past hour I tried to extract the error that occurred but somehow my knowledge of node is not sufficient and I am running a little out of time so... explaining in words:
Now I ended up with the output that looked something like that:
I do not know which exact event triggered Zalgo. But the error message I was left with took me very long to figure out what was actually happening. Note: I am not 100% sure that this is exactly what happened but that is what I could grasp & remember. |
I fixed a lot of these issues recently. In most cases all it took was wrapping the callback with |
I had some case where the second error response was called during the callback of the first error response in an async call before the callback could be replaced with a noop. That resulted in the error case where the callback was called twice with an error message. That was pretty hard to debug. I figured that the same error could occur at other places as well and changed it there too. It would be nice if someone could provide unit tests for these changes, else I will provide them as soon as I have time for it.