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: do not swallow errors with async iterators and pipeline #32051

Conversation

mcollina
Copy link
Member

@mcollina mcollina commented Mar 2, 2020

Before this patch, pipeline() could swallow errors by pre-emptively
producing a ERR_STREAM_PREMATURE_CLOSE that was not really helpful
to the user.

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

@nodejs-github-bot
Copy link
Collaborator

@ronag
Copy link
Member

ronag commented Mar 2, 2020

Something else is going on here. I don't understand how this can cause premature close. It seems that the catch clause in pump is never reached which I find strange.

The test can be further simplified to:

{
  const rs = new Readable({
    read() {
      setImmediate(() => {
        rs.push('hello');
      });
    }
  });
  pipeline(rs, async function*(stream) {
    /* eslint no-unused-vars: off */
    for await (const chunk of stream) {
      throw new Error('kaboom');
    }
  }, common.mustCall((err) => {
    assert.strictEqual(err.message, 'kaboom');
  }));
}

EDIT:

The throw inside the iterable seems to call iterator.return instead of iterator.throw which I find strange, but maybe that's the way it's supposed to work?

    for await (const chunk of stream) {
      throw new Error('kaboom');
    }

this will call iterator.return() on the stream iterator which in turn will call stream.destroy() and not propagate the error directly. I might need to read up on the iterator protocol. I would have assumed that it would call iterator.throw(err).

@mcollina
Copy link
Member Author

mcollina commented Mar 2, 2020

I thought the same as well, and I do not understand why the catch call is never reached. I was not be able to reproduce the problem with just our async iterator code.

The “bug” comes from the interaction between all the streams and the async iterator.

Not that for an async iterator in pipeline we have 3 finished() calls that gets into play (1 in the main async iterator, 1 inside the finish function called from return, and one inside pipeline).

The internal error is swallowed because the iterator ends abruptly and it causes the other streams to close. Those premature closes are forwarded earlier than the error from the internal async iterator. At least, that’s my read of the problem.

Let me know if you can crack this :/.

@mcollina
Copy link
Member Author

mcollina commented Mar 2, 2020

The throw inside the iterable seems to call iterator.return instead of iterator.throw which I find strange, but maybe that's the way it's supposed to work?

That’s what I discovered as well. I think it’s how it works.

@ronag ronag closed this Mar 2, 2020
@ronag ronag reopened this Mar 2, 2020
@ronag
Copy link
Member

ronag commented Mar 2, 2020

Sorry, the close was a misclick.

I don't like this solution but I can't really think of anything better...

Let me know if you can crack this :/.

If this is not an urgent issue please give me a few days to try to crack it before landing this.

@devsnek
Copy link
Member

devsnek commented Mar 2, 2020

The throw inside the iterable seems to call iterator.return instead of iterator.throw which I find strange, but maybe that's the way it's supposed to work?

I can confirm this is the correct behaviour (I have absolutely no idea why though, I'd have to dig into old tc39 notes)

@ronag
Copy link
Member

ronag commented Mar 2, 2020

@devsnek Do you have any advice here? We had a short mail conversation in regards to a related topic some time ago.

In particular, I'm curious why the iterator protocol would not call iterator.throw(err) when an error occurs inside the for block. Is there anyway to make it so, e.g. with some kind of hack using yield*? See the later part of this comment.

EDIT: wow, nice timing @devsnek

@devsnek
Copy link
Member

devsnek commented Mar 3, 2020

Ok this is basically a difference in the node stream model and the async iterator model. In node.js, the streams own errors that happen while they are being consumed (emit('data') will throw if one of the handlers throws), but in async iterators, they do not.

@bakkot
Copy link

bakkot commented Mar 3, 2020

To expand a bit:

My mental model of GeneratorPrototype.throw is that it is for cases where the generator needs a value to continue execution which the consumer is unable to supply because it has hit an error while attempting to produce that value. It is not for cases where the consumer hits an error while just passively consuming the generator's output; that's the consumer's problem.

@mcollina mcollina force-pushed the fix-error-swallowing-pipeline-async-iterators branch from 6ac2f87 to 89b2368 Compare March 3, 2020 15:23
@mcollina
Copy link
Member Author

mcollina commented Mar 3, 2020

@ronag this should be a better fix. The problem we were facing was a race between the ERR_STREAM_PREMATURE_CLOSE of the last stream and the error produced by the async iteration. I've solved the bug by having the callback called when all streams have been successfully destroyed and just not the last.

Copy link
Member

@ronag ronag left a comment

Choose a reason for hiding this comment

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

Nice! A few questions though.

Why not just set finishCount to the number of calls to wrap? That way you only need to decrement it in the destroyer callback.

jasnell
jasnell previously approved these changes Mar 7, 2020
@mcollina mcollina force-pushed the fix-error-swallowing-pipeline-async-iterators branch from 77019be to 4d34ee6 Compare March 9, 2020 19:38
Before this patch, pipeline() could swallow errors by pre-emptively
producing a ERR_STREAM_PREMATURE_CLOSE that was not really helpful
to the user.

Co-Authored-By: Robert Nagy <ronagy@icloud.com>
@mcollina mcollina force-pushed the fix-error-swallowing-pipeline-async-iterators branch from 4d34ee6 to e4c3f55 Compare March 9, 2020 19:52
@mcollina
Copy link
Member Author

mcollina commented Mar 9, 2020

@mcollina mcollina dismissed jasnell’s stale review March 9, 2020 19:55

it changed too much since last review, PTAL.

@mcollina
Copy link
Member Author

mcollina commented Mar 9, 2020

@ronag @jasnell @addaleax @benjamingr PTAL.

@nodejs-github-bot
Copy link
Collaborator

lib/internal/streams/pipeline.js Outdated Show resolved Hide resolved
Copy link
Member

@ronag ronag left a comment

Choose a reason for hiding this comment

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

Would prefer to keep final in destroyer

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

mcollina added a commit that referenced this pull request Mar 11, 2020
Before this patch, pipeline() could swallow errors by pre-emptively
producing a ERR_STREAM_PREMATURE_CLOSE that was not really helpful
to the user.

Co-Authored-By: Robert Nagy <ronagy@icloud.com>

PR-URL: #32051
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
@mcollina
Copy link
Member Author

Landed as d7fe554

@mcollina mcollina closed this Mar 11, 2020
MylesBorins pushed a commit that referenced this pull request Mar 11, 2020
Before this patch, pipeline() could swallow errors by pre-emptively
producing a ERR_STREAM_PREMATURE_CLOSE that was not really helpful
to the user.

Co-Authored-By: Robert Nagy <ronagy@icloud.com>

PR-URL: #32051
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
@MylesBorins MylesBorins mentioned this pull request Mar 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants