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

v16.5.0 change in async writable._final() behaviour #39535

Closed
tchetwin opened this issue Jul 26, 2021 · 10 comments
Closed

v16.5.0 change in async writable._final() behaviour #39535

tchetwin opened this issue Jul 26, 2021 · 10 comments
Labels
stream Issues and PRs related to the stream subsystem.

Comments

@tchetwin
Copy link
Contributor

tchetwin commented Jul 26, 2021

Version

16.5.0

Platform

Microsoft Windows NT 10.0.19041.0 x64

Subsystem

stream

What steps will reproduce the bug?

As a result of #39329, a program that works in v16.4.2 no longer works in v16.5.0.
The issue occurs when an async writable._final() implementation explicitly calls the callback argument.
In v16.5.0 the callback-call and async-resolve combined result in [ERR_MULTIPLE_CALLBACK]: Callback called multiple times in some circumstances.

import { Duplex, Readable } from "stream";
import { setTimeout } from "timers/promises";

const slowDuplex = new Duplex({
    read(_size) {
        setTimeout(1000).then(() => {
            this.push("abc\n");
            this.push(null);
        })
    },
    async final(cb) {
        // both this `cb()` and the `_final()` Promise resolving
        // will trigger `onFinish` under the hood.
        cb();
    },
});

slowDuplex.on("error", (err) => {
    console.log(`"error" fired\n  ${err}`);
});

Readable.from([]).pipe(slowDuplex).pipe(process.stdout);

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior?

$ nvm run 16.4.2 repro.mjs
Running node v16.4.2 (npm v7.18.1)
abc

What do you see instead?

$ nvm run 16.5.0 repro.mjs
Running node v16.5.0 (npm v7.19.1)
"error" fired
  Error [ERR_MULTIPLE_CALLBACK]: Callback called multiple times

Additional information

Documentation

I don't believe the _final() documentation sets expectations about the behaviour when it is async (or a Promise returner). The source code before and after the change in question seems to point towards it being an intended means of implementing the _final().

(Unless I've missed a part of the documentation): Users equipped only with this documentation and without looking at the implementation may be relying on the previous behaviour.

Types

The DefinitelyTyped types could potentially encode this expectation with an overload for void-returners having the argument, and Promise-returners not having the argument.

@targos
Copy link
Member

targos commented Jul 27, 2021

@nodejs/streams

@targos targos added the stream Issues and PRs related to the stream subsystem. label Jul 27, 2021
@ronag
Copy link
Member

ronag commented Jul 27, 2021

IMO the current behavior is correct. I think both async and cb should error like this but maybe not on v16 and docs might need some improvement.

Maybe a v16 fix or revert?

@mcollina
Copy link
Member

The quickest solution is do not use async in streams method. Unless noted, the API was designed in a pre-promise era and it might not like the fact that async returns a Promise.

(We should probably put this in the doc).

@ronag
Copy link
Member

ronag commented Jul 27, 2021

and it might not like the fact that async returns a Promise.

Ouch, yea, we are missing a Promise.resolve(thenable) to ensure recursive resolve. Adding promise support was maybe not the best idea :/.

@robpalme
Copy link

For clarity, the above issue is something that affected a home-grown Gulp-based compiler used in production. We've already fixed the compiler itself, so it is now compatible with Node 16.5, so all is well for new releases.

Unfortunately this behavior-change will block upgrading the machine that runs the compilation service to use Node >16.4 until we've patched all packages that use old versions of the compiler. Those packages are not owned by us, so it means convincing other teams to do work. I'd estimate it would delay our upgrade by around one year. This is inconvenient but not fatal - I pass it on not to request any particular action/work, purely for awareness. Ultimately, it looks like the behavior-change is the right way forwards, so it should go ahead either now or later.

@mcollina
Copy link
Member

mcollina commented Jul 28, 2021

@ronag I think we should do a revert :( and possibly land the change in v17.

@robpalme would that be ok?

@robpalme
Copy link

@mcollina A revert would be welcome, but it's not essential. The key is to avoid backporting the break to 14.x.

There's no need to delay progress if you think we're the only party affected here. (I wrote the offending code so it's on me anyway 😉)

@paulrutter
Copy link

paulrutter commented Nov 9, 2021

Any progress on this issue @mcollina?
I'm running into this issue as well, although i haven't been able to isolate it yet in our codebase.

Error [ERR_MULTIPLE_CALLBACK]: Callback called multiple times
    at NodeError (node:internal/errors:371:5)
    at onFinish (node:internal/streams/writable:667:37)
    at processTicksAndRejections (node:internal/process/task_queues:82:21)

Does this go for other stream methods (like transform and flush) as well?
Anything we can do about it, except fixing the code to either use callbacks or promises (e.g. async functions)?

The latter might be hard when using NPM dependencies, so i'd rather have it "fixed" in Node.js where possible.
I still think it should be ok to just use the callback (although not very neat), while making the function itself async, since that makes it easier to use await in those methods.

Node version: 16.13.0

Update
Found the relevant piece of code:

    const responseStream = new Transform({
      objectMode: true,
      transform: (chunk, enc, next) => next(null, chunk),
      final: cb =>
        myInstance.close()
          .then(cb)
          .catch(err => responseStream.emit("error", err))
    });

This causes the final method to return a promise, which in turn will call the onFinish method two times.

ronag added a commit to nxtedition/node that referenced this issue Nov 10, 2021
ronag added a commit to nxtedition/node that referenced this issue Nov 10, 2021
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535
ronag added a commit to nxtedition/node that referenced this issue Nov 10, 2021
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535
@mcollina
Copy link
Member

@ronag is on it.

ronag added a commit that referenced this issue Nov 14, 2021
Refs: #39535

PR-URL: #40772
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: James M Snell <[email protected]>
ronag added a commit to nxtedition/node that referenced this issue Nov 14, 2021
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535
@ronag
Copy link
Member

ronag commented Nov 14, 2021

Fixed in afe460e

@ronag ronag closed this as completed Nov 14, 2021
aduh95 pushed a commit to aduh95/node that referenced this issue Nov 18, 2021
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: nodejs#39535
aduh95 added a commit to aduh95/node that referenced this issue Nov 18, 2021
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: nodejs#39535

Co-authored-by: Antoine du Hamel <[email protected]>
targos pushed a commit that referenced this issue Nov 21, 2021
Refs: #39535

PR-URL: #40772
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: James M Snell <[email protected]>
nodejs-github-bot pushed a commit that referenced this issue Nov 26, 2021
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: #39535

Co-authored-by: Antoine du Hamel <[email protected]>

PR-URL: #40860
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: James M Snell <[email protected]>
targos pushed a commit that referenced this issue Nov 26, 2021
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: #39535

Co-authored-by: Antoine du Hamel <[email protected]>

PR-URL: #40860
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: James M Snell <[email protected]>
ronag added a commit to nxtedition/node that referenced this issue Dec 5, 2021
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535
danielleadams pushed a commit that referenced this issue Jan 30, 2022
Refs: #39535

PR-URL: #40772
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: James M Snell <[email protected]>
danielleadams pushed a commit that referenced this issue Jan 30, 2022
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: #39535

Co-authored-by: Antoine du Hamel <[email protected]>

PR-URL: #40860
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: James M Snell <[email protected]>
danielleadams pushed a commit that referenced this issue Feb 1, 2022
Refs: #39535

PR-URL: #40772
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: James M Snell <[email protected]>
danielleadams pushed a commit that referenced this issue Feb 1, 2022
Deprecate support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth, and never was documented.

Refs: #39535

Co-authored-by: Antoine du Hamel <[email protected]>

PR-URL: #40860
Reviewed-By: Robert Nagy <[email protected]>
Reviewed-By: James M Snell <[email protected]>
ronag added a commit to nxtedition/node that referenced this issue Apr 6, 2022
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535
nodejs-github-bot pushed a commit that referenced this issue Apr 8, 2022
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: #39535

PR-URL: #40773
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
xtx1130 pushed a commit to xtx1130/node that referenced this issue Apr 25, 2022
Remove support for returning thenables in stream
implementation methods. This is causing more confusion
and issues than it's worth.

Refs: nodejs#39535

PR-URL: nodejs#40773
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

No branches or pull requests

6 participants