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

socket: don't emit premature 'close' #20745

Closed

Conversation

ronag
Copy link
Member

@ronag ronag commented May 15, 2018

I'm not sure how to get this to work properly nor how to create a test for it but I think there is an issue here to fix.

Looking at the current net.Socket implementation I see a few potential problems:

  • 'close' can be emitted twice. Once from https://github.com/nodejs/node/blob/master/lib/net.js#L597 and once from https://github.com/nodejs/node/blob/master/lib/net.js#L604. Once with hadError argument and once without.

  • EDIT: Doesn't emit close if !_handle.

  • EDIT: _server._emitCloseIfDrained before socket is closed.

  • 'close' can be prematurely emitted before the handle is actually closed.

  • 'close' has an argument which doesn't conform with the streams spec.

  • if the handle calls the callback synchronously then 'close' is emitted while destroyed !== false.

  • Some user land code expect 'close' to have no arguments, e.g.

function onComplete (err) {
  if (err) {
    ...
  } else {
    ...
  }
}

socket.on('error', onComplete)
socket.on('close', onComplete)

Also, I believe the spec for stream events says that 'close' has no arguments?

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

@nodejs-github-bot nodejs-github-bot added the net Issues and PRs related to the net subsystem. label May 15, 2018
@lpinca
Copy link
Member

lpinca commented May 15, 2018

Good catch, it looks like a regression introduced with a7c25b7

Also, I believe the spec for stream events says that 'close' has no arguments?

hadError is emitted along with the event for net.Socket

cc: @mafintosh

@BridgeAR
Copy link
Member

@nodejs/streams @nodejs/http2 @mcollina @apapirovski PTAL

@ronag
Copy link
Member Author

ronag commented May 18, 2018

hadError is emitted along with the event for net.Socket

This is a bit difficult to implement currently though, since close is emitted by the base class and there is currently no good way to do this without overriding Stream.destroy or (as before) emitting close twice.

@ronag
Copy link
Member Author

ronag commented May 18, 2018

Related, #20755

@ronag

This comment has been minimized.

@lpinca lpinca added the confirmed-bug Issues with confirmed bugs. label May 18, 2018
@alexjeffburke
Copy link
Contributor

Sorry to have to ask but this feels like something that could cause quite subtle changes in behaviour - I can’t figure out whether this shipped in node 10, but another related change was previously reverted. If it has shipped, should the original commit be reverted for the 10.2 release?

@ronag

This comment has been minimized.

@lpinca

This comment has been minimized.

@ronag

This comment has been minimized.

@ronag

This comment has been minimized.

@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch from 25a6f51 to be6aee8 Compare May 20, 2018 09:11
@ronag

This comment has been minimized.

@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch 7 times, most recently from d373e29 to 002c1a0 Compare May 20, 2018 09:23
@lpinca lpinca removed the confirmed-bug Issues with confirmed bugs. label May 20, 2018
@lpinca
Copy link
Member

lpinca commented May 20, 2018

Correct, I missed this commit 5e3f516.

Can you please update the PR description?

  • 'close' can be prematurely emitted before the handle is actually closed.

No, it is emitted when the handle is closed.

if the handle calls the callback synchronously then 'close' is emitted while destroyed !== false.

The callback of _handle.close() is not called synchronously.

To summarise, only the following points are valid:

  • Doesn't emit close if !_handle
  • 'close' has an argument which doesn't conform with the streams spec.

I think both of them are expected and work as intended.

@ronag
Copy link
Member Author

ronag commented May 20, 2018

@lpinca what I'm still unsure about is since error will be emitted at the next tick which is scheduled AFTER handle.close whether error might be emitted after close.

@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch 6 times, most recently from f39a17d to e112cdd Compare May 20, 2018 11:38
@lpinca
Copy link
Member

lpinca commented May 20, 2018

That doesn't seem to happen, it is hopefully covered by tests and if not it would have already been reported.

lib/net.js Outdated
const onClosed = () => {
debug('emit close');
if (exception) {
this.emit('error', exception)
Copy link
Member

Choose a reason for hiding this comment

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

If I'm not wrong this will emit an error every time socket.destroy() is called with an error.

Copy link
Member Author

Choose a reason for hiding this comment

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

That will happen through cb(exception) as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, you are right, this is quite complicated...

@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch 8 times, most recently from 22d7871 to 6fca676 Compare May 20, 2018 12:17
@alexjeffburke
Copy link
Contributor

@ronag @lpinca thanks both very much for continuing to dig into this. The reason I somewhat pushed back a bit was I remember many times we've have to on('error', ..) and on('close', ..) in codebases and use a 'sawEnd` boolean to make sure we detect teardown situations but avoid cleanup code being called twice due to these events etc. Hopefully that helps explains my earlier nerves a little more :)

@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch 5 times, most recently from e57f613 to 347fe56 Compare May 20, 2018 13:00
@ronag ronag force-pushed the socket-fix-wait-for-handle-close branch from 347fe56 to 22b4ab7 Compare May 20, 2018 13:07
@lpinca
Copy link
Member

lpinca commented May 20, 2018

I'm not going to block this but I'm not very happy with removing the hadError argument. net and fs streams are a bit special and userland modules have been modelled around their behavior over the years. Removing an argument that has been there since version v0.1.90 for the sake of consistency doesn't seem a good idea to me.

@lpinca lpinca added the semver-major PRs that contain breaking changes and should be released in the next major version. label May 20, 2018
@@ -583,33 +581,33 @@ Socket.prototype._destroy = function(exception, cb) {
clearTimeout(s[kTimeout]);
}

debug('close');
const onClosed = () => {
Copy link
Member

Choose a reason for hiding this comment

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

I didn't test but I think this still does not prevent the 'error' event from being emitted multiple times. Assume that socket.destroy(err) is called twice.

First call will wait for this callback to be called and emit 'error' and 'close' when that happens.
Second call will find _readableState.destroyed set to true but _writableState.errorEmitted is not set to true yet as that will happen when this callback if invoked, so another 'error' event will be emitted.

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 isn't that true for any stream? If you call destroy twice with error it will emit error twice... I would say the problem is in the destroy impl then?

Copy link
Member

Choose a reason for hiding this comment

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

No, it's emitted only once

const { Writable } = require('stream');

const w = new Writable();

w.on('error', console.log);
w.destroy(new Error('Oops'));
w.destroy(new Error('Oops'));

Copy link
Member Author

@ronag ronag May 20, 2018

Choose a reason for hiding this comment

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

Twice like this...

const { Writable } = require('stream');

const w = new Writable({
    destroy (err, cb) {
        process.nextTick(() => cb(err))
    }
});

w.on('error', console.log);
w.destroy(new Error('Oops'));
w.destroy(new Error('Oops'));

it's only once if you call cb synchronously. Which doesn't seem like the usual case or we wouldn't have a callback?

Copy link
Member

Choose a reason for hiding this comment

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

The callback on destroy is not a public api. It is used only internally.

Copy link
Member Author

@ronag ronag May 20, 2018

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

You are right, sorry, I was referring to https://nodejs.org/api/stream.html#stream_writable_destroy_error.

Copy link
Member

Choose a reason for hiding this comment

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

(We should fix that error being emitted more than once in streams imo.)

@ronag
Copy link
Member Author

ronag commented May 20, 2018

I'm not going to block this but I'm not very happy with removing the hadError argument. net and fs streams are a bit special and userland modules have been modelled around their behavior over the years.

Alternatively, I would suggest then that the hadError option be added to the streams API? So that future userland modules modeled around the streams API doesn't incorrectly assume no argument.

I'm mostly concerned about the unexpected difference in behavior. I've had bugs in my own code due to this.

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 -1 to land this change right now. I prefer we land this after some of the refactoring in streams described in #20096.

Anyway, a unit test would be really handy.

@jasnell
Copy link
Member

jasnell commented Sep 10, 2018

Given the -1's and the lack of progress... closing this one for now.

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

Successfully merging this pull request may close these issues.

8 participants