Skip to content

stream: call _final synchronously #30597

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

Closed
wants to merge 2 commits into from

Conversation

ronag
Copy link
Member

@ronag ronag commented Nov 22, 2019

  • We don't actually have to call _final in a nextTick we just need to emit the event asynchronously. Try to call _final as soon as possible. With this change it's possible to remove some prefinish hacks where prefinish is used to get a synchronous pre _final.

This is blocking some other pending fixes.

Blocked by #29656

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 Nov 22, 2019
@ronag
Copy link
Member Author

ronag commented Nov 22, 2019

Those failures are very interesting. WIP

@ronag ronag force-pushed the stream-fix-end-finish branch 4 times, most recently from b6777af to 9cb4bbb Compare November 23, 2019 00:54
@ronag
Copy link
Member Author

ronag commented Nov 23, 2019

@mcollina: I think you might be interested in this change.

@jasnell @addaleax The http2 failures are very strange to me. The way http2 uses streams a bit of a challenge. My gut feeling is that there is a bug hiding somewhere and this PR activates it. Would either if you be interested in collaborating with me to figure out why this PR would cause the failing tests in http2?

@ronag
Copy link
Member Author

ronag commented Nov 23, 2019

e.g. in test-http2-client-upload-reject

  server.on('stream', common.mustCall((stream) => {
    // Wait for some data to come through.
    setImmediate(() => {
      stream.on('close', common.mustCall(() => {
        assert.strictEqual(stream.rstCode, 0);
      }));

      stream.respond({ ':status': 400 });
      stream.end();
    });
  }));

stream won't emit 'close' until the reading side has also emitted 'end' (due to allowHalfOpen = true in the HTTP2Stream constructor). However, since this test never starts to read it should actually never emit 'end' and 'close' so I'm unsure how this test could have passed previously?

The rest of the failures seem related to this.

@@ -286,7 +286,8 @@ Object.setPrototypeOf(WriteStream, Writable);
WriteStream.prototype._final = function(callback) {
if (typeof this.fd !== 'number') {
return this.once('open', function() {
this._final(callback);
// Make sure _final is called after every 'open' listener
process.nextTick(() => this._final(callback));
Copy link
Member

Choose a reason for hiding this comment

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

Would this maybe be a good reason to keep _final behind a nextTick, at least in a semver-patch PR? Or maybe, why does this need to be changed here but not in the other places where we use a similar pattern (e.g. net, http2)?

Copy link
Member Author

@ronag ronag Nov 23, 2019

Choose a reason for hiding this comment

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

That's one reason to keep _final behind a nextTick but there are other reasons which make it not a good idea. I don't think this becomes a relevant problem once #29656 is merged.

Copy link
Member Author

Choose a reason for hiding this comment

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

but not in the other places where we use a similar pattern (e.g. net, http2)?

It should probably be changed in those places as well.

@ronag
Copy link
Member Author

ronag commented Nov 24, 2019

@Trott a WIP label please

@lpinca lpinca added the wip Issues and PRs that are still a work in progress. label Nov 24, 2019
@ronag ronag changed the title stream: 'finish' should always be emitted asynchronously stream: call _final synchronously Dec 7, 2019
@ronag
Copy link
Member Author

ronag commented Dec 7, 2019

This has some overlap with #30733. This is more about calling _final synchronously as to avoid 'prefinish' hacks.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

I’m positive to this change, however I would wait for #29656 to land. I’d just be cautious and flag it semver-major. All things that interact with the timing order are hard to debug.

@ronag
Copy link
Member Author

ronag commented Dec 7, 2019

Using this transform stream could be potentially significantly simplified, see https://github.com/nxtedition/node/blob/stream-transform-simplify/lib/_stream_transform.js (WIP). As well as other "sync" streams.

@ronag ronag force-pushed the stream-fix-end-finish branch 10 times, most recently from 1672b25 to 66e2654 Compare December 14, 2019 22:36
@ronag ronag requested review from mcollina and addaleax December 15, 2019 10:26
@ronag
Copy link
Member Author

ronag commented Dec 15, 2019

@mcollina @Trott this is no longer WIP

const err = handle.shutdown(req);
if (err === 1) // synchronous finish
return afterShutdown.call(req, 0);
// TODO: Why does this need to be in nextTick?
Copy link
Member Author

@ronag ronag Dec 15, 2019

Choose a reason for hiding this comment

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

@addaleax Maybe you have some insight here?

Copy link
Member

Choose a reason for hiding this comment

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

What happens when it isn’t? 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh! I forgot about that small detail:

Path: parallel/test-http2-client-socket-destroy
Mismatched noop function calls. Expected exactly 1, actual 0.
    at Object.mustCall (/Users/ronagy/GitHub/nxtedition/node/test/common/index.js:324:10)
    at Http2Server.<anonymous> (/Users/ronagy/GitHub/nxtedition/node/test/parallel/test-http2-client-socket-destroy.js:19:29)
    at Http2Server.<anonymous> (/Users/ronagy/GitHub/nxtedition/node/test/common/index.js:358:15)
    at Http2Server.emit (events.js:304:20)
    at ServerHttp2Session.sessionOnStream (internal/http2/core.js:2739:19)
    at ServerHttp2Session.emit (events.js:304:20)
    at emit (internal/http2/core.js:285:8)
    at processTicksAndRejections (internal/process/task_queues.js:87:22)
Command: out/Release/node --expose-internals /Users/ronagy/GitHub/nxtedition/node/test/parallel/test-http2-client-socket-destroy.js

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems like some form of timing issue.

@@ -82,15 +82,15 @@ const t = new stream.Transform({
process.nextTick(function() {
Copy link
Member Author

Choose a reason for hiding this comment

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

This PR makes the order here more sensible

@ronag
Copy link
Member Author

ronag commented Dec 15, 2019

This is semver-major due to the change in timing

@ronag ronag force-pushed the stream-fix-end-finish branch 2 times, most recently from 866be29 to b6f2f1a Compare December 15, 2019 15:17
@Trott Trott removed the wip Issues and PRs that are still a work in progress. label Dec 15, 2019
@BridgeAR BridgeAR added the semver-major PRs that contain breaking changes and should be released in the next major version. label Dec 25, 2019
@ronag
Copy link
Member Author

ronag commented Dec 28, 2019

@Trott: node/streams ping?

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

I’m not convinced by this change given the impact in our codebase.

@ronag
Copy link
Member Author

ronag commented Dec 28, 2019

My main motivation is that it would be significantly easier to implement streams which need to support both sync and async behavior, e.g. Transform. See for example, https://github.com/nxtedition/node/blob/stream-fix-end-finish/lib/_stream_transform.js vs https://github.com/nodejs/node/blob/master/lib/_stream_transform.js.

This is currently not possible. It's made worse by the fact that you can't properly emulate this with prefinish hacks since prefinish will be emitted sync if _final === null but async otherwise.

Though this doesn't actually "fix" anything other than make things easier to maintain. So whether it's worth the risk or not is debatable...

@ronag
Copy link
Member Author

ronag commented Dec 28, 2019

I think there are more pressing PR's to land so if this is controversial I'm fine with closing this for now and re-open at a more appropriate time.

@ronag
Copy link
Member Author

ronag commented Dec 28, 2019

The prefinish sometimes sync and sometimes async thing is something I just noticed today. Not sure if there are and what the implications of that might be, i.e. a stream might behave differently if it uses prefinish and sometimes has and sometimes doesn't have a _final.

Example:

let ticked = false;
const t1 = new Transform({
  transform(chunk, encoding, cb) {
    cb();
  }
});
t1.on('finish', () => {
  console.log('t1', ticked);
});
t1.end();

const t2 = new Transform({
  transform(chunk, encoding, cb) {
    cb();
  },
  final(callback) {
    callback();
  }
});
t2.on('finish', () => {
  console.log('t2', ticked);
});
t2.end();
ticked = true;

// Prints
// t1 false
// t2 true

Though providing a _final to a Transform stream doesn't make much sense (should use _flush). But it does illustrate the potential of difference in behavior.

@ronag ronag closed this Dec 28, 2019
@ronag ronag reopened this Dec 28, 2019
@ronag ronag force-pushed the stream-fix-end-finish branch from b6f2f1a to da175f0 Compare December 28, 2019 20:10
@ronag
Copy link
Member Author

ronag commented Dec 28, 2019

Pushed a stream: simplify Transform commit to better show why this PR is useful (still some tests failing), i.e. making it possible to implement Transform strictly in terms of Duplex without extra state and logic. I would prefer to continue working on the Transform part as a separate future PR.

@ronag ronag force-pushed the stream-fix-end-finish branch 4 times, most recently from 91618f5 to 5cf54fb Compare December 28, 2019 20:18
@ronag ronag force-pushed the stream-fix-end-finish branch 5 times, most recently from b43b23e to 63380c2 Compare December 28, 2019 21:23
@ronag ronag force-pushed the stream-fix-end-finish branch from 63380c2 to 68c0d3b Compare December 29, 2019 15:26
@ronag
Copy link
Member Author

ronag commented Dec 29, 2019

Yea, I still think this is a good idea long term but now that I started digging into it again it's significantly more complicated than I thought. We have higher prio stuff to put energy into.

I'll re-open this in the future.

@ronag ronag closed this Dec 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-major PRs that contain breaking changes and should be released in the next major version. stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants