Skip to content
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

stream: prevent 'end' to be emitted after 'error' #20104

Closed

Conversation

mcollina
Copy link
Member

This PR adds _readableState.errorEmitted and add the tracking of it.

Fixes: #6083

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the stream Issues and PRs related to the stream subsystem. label Apr 17, 2018
@mcollina
Copy link
Member Author

@mcollina
Copy link
Member Author

cc @davepacheco

Copy link
Member

@apapirovski apapirovski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but want to give it another look before full on approval.

@@ -99,6 +99,9 @@ function ReadableState(options, stream, isDuplex) {
this.endEmitted = false;
this.reading = false;

// flipped if an 'error' is emitted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: capitalize & end with a dot. (And yes, I know it totally doesn't match the rest of this file but it is our new style guide...)

@@ -424,12 +424,22 @@ function onwriteError(stream, state, sync, er, cb) {
// this can emit finish, and it will always happen
// after error
process.nextTick(finishMaybe, stream, state);

// needed for duplex, fixes https://github.com/nodejs/node/issues/6083
if (stream._readableState) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked in enough detail but I guess there's no way to do this in _stream_duplex.js instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal would be to change this into stream.destroy(err). But that's a more massive change, see #20096.

Copy link
Member

@lpinca lpinca Apr 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can't we only use one of these and put it outside the wrapper if instead of duplicating it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't as the two branches are significantly different. However we can move it in its own function. Should I do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm why, in this branch there are only two next tick calls so it's not a problem, in the else below there is only cb(er); before but I think it's not necessary to call the callback before setting the flag. I mean something like this:

diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js
index d21daf0541..04b344bc26 100644
--- a/lib/_stream_writable.js
+++ b/lib/_stream_writable.js
@@ -417,6 +417,13 @@ function doWrite(stream, state, writev, len, chunk, encoding, cb) {
 function onwriteError(stream, state, sync, er, cb) {
   --state.pendingcb;
 
+  // needed for duplex, fixes https://github.com/nodejs/node/issues/6083
+  if (stream._readableState) {
+    stream._readableState.errorEmitted = true;
+  }
+
+  stream._writableState.errorEmitted = true;
+
   if (sync) {
     // defer the callback if we are being called synchronously
     // to avoid piling up things on the stack
@@ -424,13 +431,11 @@ function onwriteError(stream, state, sync, er, cb) {
     // this can emit finish, and it will always happen
     // after error
     process.nextTick(finishMaybe, stream, state);
-    stream._writableState.errorEmitted = true;
     stream.emit('error', er);
   } else {
     // the caller expect this to happen before if
     // it is async
     cb(er);
-    stream._writableState.errorEmitted = true;
     stream.emit('error', er);
     // this can emit finish, but finish must
     // always follow error

but I didn't test it. If it doesn't work or if I am missing something, ignore me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the second block, you are setting errorEmitted before calling the callback. I remember spending a significant amount of time on this block of code, and these two paths should remain separated. It's something I'd prefer not to refactor in this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes stream._writableState.errorEmitted = true can stay as is if the user provided callback needs to find it set to false (why?) but I see no reason to not move the other part. There can't be existing code that relies on new code.

Anyway it's a just a nit and it can be ignored.

@@ -6,12 +6,15 @@ function destroy(err, cb) {
this._readableState.destroyed;
const writableDestroyed = this._writableState &&
this._writableState.destroyed;
const readableErrored = this._readableState &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we create these inside the if block just below? It doesn't look like they're used outside of it.

@apapirovski
Copy link
Member

We should probably at least let this bake before LTS if not outright not land it. The fact that we even have tests that rely on this behaviour is concerning...

@mcollina
Copy link
Member Author

We should probably at least let this bake before LTS if not outright not land it.

This should definitely bake for a bit before going into LTS.

This PR adds _readableState.errorEmitted and add the tracking of it.

Fixes: nodejs#6083
@mcollina mcollina force-pushed the add-readableState-errorEmitted branch from 9b0fd56 to 5b69058 Compare April 17, 2018 15:58
duplex.on('end', common.mustNotCall());

duplex.end('hello');
duplex.on('data', function() {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: duplex.resume() is cleaner.

@lpinca
Copy link
Member

lpinca commented Apr 17, 2018

Ops I commented on the fork.

if (cb) {
cb(err);
} else if (err &&
(!this._writableState || !this._writableState.errorEmitted)) {
} else if (err && !(readableErrored || writableErrored)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe it's just me but I find it a lot easier to read if De Morgan is applied.

@@ -21,6 +21,7 @@ if (process.argv[2] === 'child') {
assert.strictEqual(signal, null);
}));

child.stderr.pipe(process.stderr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

@mcollina
Copy link
Member Author

@mcollina
Copy link
Member Author

CI (only the ones that failed): https://ci.nodejs.org/job/node-test-commit-linux/18088/

@BridgeAR BridgeAR added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Apr 18, 2018
@BridgeAR
Copy link
Member

What is this semver wise? This should be semver-major, right?

@lpinca
Copy link
Member

lpinca commented Apr 18, 2018

@BridgeAR yes, this is semver-major imho.

@mcollina
Copy link
Member Author

This solves an extremely bad bug that has been open for a long period of time. errorEmitted has been part of Writable for a long period of time, but the same logic was never added to Readable.

The underlining contract of streams is that after 'error'  is emitted you cannot trust any other event that is being emitted. Ideally, no stream event should be emitted after 'error' is emitted.

This relies on the .destroy() that has been added on 8. In fact, it should have been part of it. I preferred not to add this logic then because I couldn't find a failing scenario, which #6083 provided.

I'm certain that this could lend on 10 as a minor or patch, due to the new error semantics that @mafintosh has added in #19836. In fact, it should be in as a way to strengthen that invariant.

As of backporting to 8, this is possible, as the current behavior is not deterministic (the test case in #6083 fails 50% of the time).

@mafintosh, what do you think?

@mcollina mcollina added the baking-for-lts PRs that need to wait before landing in a LTS release. label Apr 18, 2018
@mcollina mcollina requested a review from a team April 18, 2018 18:12
@jasnell
Copy link
Member

jasnell commented Apr 19, 2018

Still LGTM

@mafintosh
Copy link
Member

LGTM from me. I'm noting that in my userland stream modules we already consider the first error to be "terminal", and ignore any following events because you can't rely on their behaivor after an error. Good job @mcollina

This is a semver-minor, as it's a bug fix.

I would only attempt a backport if it is easy to do so (you know the complexity of this more than me @mcollina), as the userland modules handles these cases right now either way.

@mcollina mcollina added the semver-minor PRs that contain new features and should be released in the next minor version. label Apr 20, 2018
@mcollina mcollina added this to the 10.0.0 milestone Apr 20, 2018
@mcollina
Copy link
Member Author

landing as a semver-minor. It would be fantastic if this could go into Node 10 @jasnell. Feel free to remove if from the milestone if it's over the finishing line.

@lpinca
Copy link
Member

lpinca commented Apr 20, 2018

If it's a bug fix why semver-minor, it doesn't add anything that should be publicly accessible.

@mcollina mcollina removed the semver-minor PRs that contain new features and should be released in the next minor version. label Apr 20, 2018
@mcollina
Copy link
Member Author

removed semver-minor.

@mcollina
Copy link
Member Author

@mcollina
Copy link
Member Author

Landed in 0857790

@mcollina mcollina closed this Apr 20, 2018
@mcollina mcollina deleted the add-readableState-errorEmitted branch April 20, 2018 13:13
mcollina added a commit that referenced this pull request Apr 20, 2018
This PR adds _readableState.errorEmitted and add the tracking of it.

Fixes: #6083

PR-URL: #20104
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
jasnell pushed a commit that referenced this pull request Apr 20, 2018
This PR adds _readableState.errorEmitted and add the tracking of it.

Fixes: #6083

PR-URL: #20104
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
@mscdex
Copy link
Contributor

mscdex commented Apr 25, 2018

If we are to keep this change, I think this should be marked semver-major as it is a change in behavior and can break existing code (I stumbled upon this myself when trying to debug with node v10). There is not even any indication that I could find in the documentation that this is the intended behavior. Otherwise, we should revert this IMO because it didn't even have time to land in a v9 release and was merged only 5 days ago.

/cc @nodejs/collaborators

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants