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

[REG 20.9->20.10] stream: Pipe is stopped after error on another pipe from the same source #53185

Open
orgads opened this issue May 28, 2024 · 7 comments
Labels
regression Issues related to regressions. stream Issues and PRs related to the stream subsystem.

Comments

@orgads
Copy link
Contributor

orgads commented May 28, 2024

Version

20.13.1

Platform

Microsoft Windows NT 10.0.19045.0 x64

Subsystem

stream

What steps will reproduce the bug?

import assert from 'assert';
import stream from 'stream';

class SizeCounter extends stream.Writable {
  constructor(name, fail) {
    super();
    this.totalSize = 0;
    this.name = name;
    this.fail = fail;
  }

  _write(chunk, _encoding, callback) {
    this.totalSize += chunk.length;
    console.log(this.name, this.totalSize);
    if (this.fail)
      return callback(new Error('You asked me to fail'));
    return callback();
  }

  _final(callback) {
    console.log(`${this.name} Total size: ${this.totalSize}`);
    callback();
  }
}

const src = new stream.PassThrough();
const dst1 = new SizeCounter('1', true);
const dst2 = new SizeCounter('2', false);

src.write(Buffer.alloc(20000));
src.write(Buffer.alloc(20000));
src.end();

dst1.on('error', () => {
  // uncomment as a workaround
  // src.resume();
});

stream.finished(src, () => console.log('src ended'));
stream.finished(dst1, (err) => console.log('dst1 ended', err?.message));
stream.finished(dst2, (err) => console.log('dst2 ended', err?.message));

src.pipe(dst1, { end: true });
src.pipe(dst2, { end: true });
setImmediate(() => assert.strictEqual(dst2.totalSize, 40000));

Output with 20.9.0:

1 20000
2 20000
dst1 ended You asked me to fail
2 40000
2 Total size: 40000
src ended
dst2 ended undefined

Output with 20.10.0:

1 20000
2 20000
dst1 ended You asked me to fail
node:assert:125
  throw new AssertionError(obj);
  ^

AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

20000 !== 40000

    at Immediate._onImmediate (file:///home/orgads/test/test.mjs:45:27)
    at process.processImmediate (node:internal/timers:478:21) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: 20000,
  expected: 40000,
  operator: 'strictEqual'
}

Node.js v20.10.0

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

Always

What is the expected behavior? Why is that the expected behavior?

I expect the stream to keep draining (at least when it still has listeners to data).

What do you see instead?

The stream is paused.

Additional information

Bisected to #50014.

Related bug:

@orgads
Copy link
Contributor Author

orgads commented May 28, 2024

@ronag @mcollina @benjamingr

@ronag
Copy link
Member

ronag commented May 29, 2024

I'm not sure I would consider this a bug. The stream should stop IMO. However, it is a semver-major change of behavior that should not have landed on v20.0.

@orgads
Copy link
Contributor Author

orgads commented May 29, 2024

Why should the stream stop if it still has other listeners on 'data'?

@ronag
Copy link
Member

ronag commented May 29, 2024

when piping to multiple destinations the data flow should not be faster than the slowest destintation (this is the behavior in the non failing case). A failed destination is no longer consuming and should therefore stop the flow. Just like in the non-error flow.

@orgads
Copy link
Contributor Author

orgads commented May 29, 2024

I see. It's worth at least documenting this change in "Notable changes".

@VoltrexKeyva VoltrexKeyva added the stream Issues and PRs related to the stream subsystem. label May 29, 2024
@mcollina
Copy link
Member

when piping to multiple destinations the data flow should not be faster than the slowest destintation (this is the behavior in the non failing case). A failed destination is no longer consuming and should therefore stop the flow. Just like in the non-error flow.

I have a rough feeling this is not what the user would expect. If I'm piping to two streams, I don't want one of the two pipes to end, surprisingly, because the other had an unrelated failure. While we must slow down at the pace of the slowest consumer, we should definitely not break the other user. I think we should consider either reverting or fixing this.

Wdyt @ronag?

Note that this kind of double-piping is problematic in many other ways, so it might not be useful to revert. If we consider this behavior as ok, we should consider adding a clone() or bring https://github.com/mcollina/cloneable-readable into core.

@mcollina mcollina added the regression Issues related to regressions. label Jun 14, 2024
@ronag
Copy link
Member

ronag commented Jun 14, 2024

I'll have a look if we can fix this so it behaves as it used to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
regression Issues related to regressions. stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

No branches or pull requests

4 participants