[BUGFIX #7924] Fix Race in yarn pack stream code#8190
Open
stefanpenner wants to merge 1 commit intoyarnpkg:1.22-stablefrom
Open
[BUGFIX #7924] Fix Race in yarn pack stream code#8190stefanpenner wants to merge 1 commit intoyarnpkg:1.22-stablefrom
yarn pack stream code#8190stefanpenner wants to merge 1 commit intoyarnpkg:1.22-stablefrom
Conversation
35510ea to
2a6e026
Compare
3 tasks
In some versions of Node.js (such as 12.16.2), yarn pack no-longer was running the `postpack` hook. Debugging the code in question (following example) lead us to notice that the following await never progressed, the streams `error` and `close` handlers were never invoked. In-fact the process was appearing to exit prematurely.
```js
// src/cli/commands/pack.js
await new Promise((resolve, reject) => {
stream.pipe(fs2.createWriteStream(filename));
stream.on(‘error’, reject); // reject is never invoked
stream.on(‘close’, resolve); // resolve is never invoked
// reached
});
// never reached
```
As it turns out, the above stream code is unsafe, and only appeared
to function do to a specific implementation detail of `zlib.Gzip`. Once that implementation detail changed in Node.js; the process would exit
while awaiting the above promise, and thus would leave the code which triggers the `postpack` hook unreachable.
1. a Node.js process exits once it's event queue has been drained and has no outstanding reference-able handles (such as IO, Timers etc).
2. stream.pipe(…) does not add a task to the event queue
3. new Promise (…) does not add a task to the event queue
4. prior to node 2.16, an implementation of `zlib.Gzip` added a task to the event queue
5. nodejs/node@0e89b64 changed that behavior (confirmed via bisect)
6. in node 2.16 (and several other versions) `yarn pack` would exit prior to invoking the `postpack` hook.
Mystery solved!
Luckily Node.js has a new utility method (node >= 10) `const { pipeline } = require(‘stream’);`
This higher level utility has less caveats than `stream.pipe` / `stream.on(…)`.
and appears to be preferred in Node.js's own documentation has been re-worked to use `pipeline` for all but the most specific low level operations.
In short, rather then:
```js
stream.pipe(otherStream);
`"
Under most circumstances it is likely wise to use
```js
const { pipeline } = require(‘stream’);
pipeline(stream, otherStream, err => {
// node-back
});
```
And if you are using streams with promises, consider first promisifying `pipeline`
```js
const { promisify } = require(‘util’);
const pipeline = promisify(require(‘stream’).pipeline)
```
7 tasks
danez
approved these changes
Sep 30, 2020
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TODO:
In some versions of Node.js which include nodejs/node@0e89b64 (such as 12.16.2) ,
yarn packno-longer runspostpack. Debugging the code in question (following example) lead us to notice that the followingawaitnever progressed, the streamserrorandclosehandlers were never invoked, and that the process was appearing to exit prematurely.What is going on here?
As it turns out, the above stream code is unsafe, and only appeared to function do to a specific implementation detail of
zlib.Gzip. Once that implementation detail changed in Node.js; the process would exit while awaiting the above promise, and thus would leave the code which triggers thepostpackhook unreachable.The facts (As we know them so far):
zlib.Gzipadded a task to the event queueyarn packwould exit prior to invoking thepostpackhook.Mystery solved!
That's a lot going on, so how can one safely use streams ?
Luckily Node.js has a new utility method (node >= 10)
const { pipeline } = require(‘stream’);This higher level utility has less caveats thanstream.pipe/stream.on(…)and appears to be preferred in Node.js's own documentation has been re-worked to usepipelinefor all but the most specific low level operations.In short, rather then:
Under most circumstances it is likely wise to use
And if you are using streams with promises, consider first promisifying
pipeline@krisselden provided validation of the issue, updated my mental model of streams and ultimately the appropriate most resilient solution.