Send stream failures up the promise chain#1
Conversation
Prior to the change, race conditions could cause errors on the stream to be lost. The symptom for npm users would be the infamous "cb() never called" error.
|
The test in the original PR fails consistently for me without this patch, and passes consistently with it. It's a race condition, so yeah, I wouldn't be surprised if the test passes spuriously sometimes, but it's still better than nothing. Reading through the code, this is clearly a race condition, and the correct way to fix it. Good job. I'll cut a release with this shortly. |
|
Hey, interesting news! So, as of this commit, the The problem was that the test was installing a package from the test registry, and getting the wrong integrity value. But, always the same integrity value was desired, and always the same one was showing up instead, so it seemed pretty deterministic. Git bisect found this as the first bad commit. After a few hours of digging, I discovered that it was getting an integrity value from the package-lock.json file from a prior test's install which wasn't cleaned up. (The test was removing ⚡️ this patch exposes a class of integrity issues which were completely ignored before ⚡️ Good job. I'm even more convinced that this was a legit problem before you found and fixed it. Thanks! |
|
That's great! I suspected this fix was probably going to solve a bunch of difficult-to-diagnose issues, but I did not know how far it would reach. |
The problematic code prior to this PR is this:
There's a race condition in this code. The problem is that neither of the
tarStream.on('error', reject)lines are capable of reporting all errors withtarStream. If atarStreamerror happens prior to the outer resolve being called, then the firsttarStream.on('error'...will pass the error up correctly. (Actually, I don't think this case is possible. At some point I had instrumented the callback passed to both calls totarStream.on('error'...and the callback passed tosetImmediate(...), and it never ever ever ever ever happened that the firsttarStream.on('error'caught an error prior to the call scheduled withsetImmediate.)However, for those errors that cannot be detected immediately, the first
tarStream.on('error'call is useless. By the time these errors are raised, the resolve call scheduled bysetImmediatewill have happened, and thus even if the firsttarStream.on('error'calls reject, this will have no effect whatsoever because the promise has already resolved.So there is a window of time between the first
tarStream.on('error'and the second one where if atarStreamerror occurs, it won't be sent up the promise chain. How big a window it is depends on how longrimrafandmkdirptake to do their work. If atarStreamerror is raised during that window, the first error event handler cannot do anything about it, and the 2nd event handler is not setup yet so it cannot do anything either.When I first filed this issue in the wrong place I had included a test, but further testing indicates that the test was not reliable. Replicating race conditions in a stable way is extremely hard unless the test can get control of all the conditions that participate in the race so that the events that lead to the failure can always be replicated in the same order. I thought I had it, but nope... hope springs eternal I guess.
The problem I initially ran into was that I was running
npmin a Docker build wheregitwas not installed, and I got the utterly non-descriptivecb() never callederror message. There was no other message with it. I've seen cases wherecb() never calledis accompanied with some more information but in my case that was the sole error message and--loglevel=sillydid not help either. The versions involved were Node v12.5.0 and npm v6.9.2. This macro-issue can be replicated in the following way:Create a new temporary directory for the test, and move into it.
We want to create a directory that contains just enough executables to allow
npmto run overall but does not allow it to rungitspecifically:package.jsonwith this:(The main thing here is to have a dependency that requires the use of
git.)(This first run of
npm installto do a real install is necessary. IfPATH=jailis used here,npmwill error but the error message is useful.)This results in:
Running the same, with the PR being proposed here, results in:
Again, this is a race condition so I would not be surprised if some folks try the above and don't get
cb() never called.