Skip to content

Readable.pipe() behaves inconsistently resuming (or not) the source #41785

Closed
@tufosa

Description

@tufosa

Version

14.17.0

Platform

Linux tufopad 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Subsystem

stream

What steps will reproduce the bug?

Prior to v14.17.0 (versions <= v14.16.1), calling to Readable.pipe() would always resume the source if it had been previously paused. This behaviour was not documented, but it was consistent (it always happened). Since version 14.17.0 this behaviour changed and now it only resumes the source in some cases. If you run the following code

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

// FIRST EXPERIMENT
console.info('********** FIRST EXPERIMENT **********');
const source1 = new PassThrough();
const target1 = new PassThrough();

// `Readable.pipe()` resumes the source if it was previously paused
source1.pause();
console.info(`source1 before pipe. Paused: ${source1.isPaused()}`);
source1.pipe(target1);
console.info(`source1 after pipe. Paused: ${source1.isPaused()}`);

// SECOND EXPERIMENT
console.info('\n********** SECOND EXPERIMENT **********');
const source2 = new PassThrough();
const target2 = new PassThrough();

// stall target2
const chunk = Buffer.allocUnsafe(1000);
let chunks = 1;
while (target2.write(chunk)) chunks++;
console.info(`${chunks} chunks of ${chunk.length} bytes to stall target2`);

// `Readable.pipe()` DOES NOT resume the source if it was previously paused, but
// the target needs drain (only in version >= v14.17.0)
source2.pause();
console.info(`source2 before pipe. Paused: ${source2.isPaused()}`);
source2.pipe(target2);
console.info(`source2 after pipe. Paused: ${source2.isPaused()}`);
target2.on('drain', () => {   
  console.info('target2 drained');
  console.info(`source2 after drain. Paused: ${source2.isPaused()}`);
});
target2.on('data', () => {});

with version v14.16.1 you will get the following output

********** FIRST EXPERIMENT **********
source1 before pipe. Paused: true
source1 after pipe. Paused: false

********** SECOND EXPERIMENT **********
34 chunks of 1000 bytes to stall target2
source2 before pipe. Paused: true
source2 after pipe. Paused: false
target2 drained
source2 after drain. Paused: false

whereas with version v14.17.0 you get

********** FIRST EXPERIMENT **********
source1 before pipe. Paused: true
source1 after pipe. Paused: false

********** SECOND EXPERIMENT **********
34 chunks of 1000 bytes to stall target2
source2 before pipe. Paused: true
source2 after pipe. Paused: true
target2 drained
source2 after drain. Paused: true

In versions higher or equal to v14.17.0, the source is NOT resumed if the target needs a drain, and it does not resume even when the target drains. Furthermore, if the source wasn't paused, Readable.pipe() will pause it if the piped target needs to drain. This did not happen in versions <= v14.16.1.

The fact that Readable.pipe() decides if it should pause or resume the source depending on the status of the target looks a bit inconsistent to me, and if not fixed, I believe that at least it would need to be documented.

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

It always happens

What is the expected behavior?

It's a change of an undocumented behaviour, so it's hard for me to say what's the expected behaviour. The previous behaviour (resuming always the source) seemed a bit more consistent than the current

What do you see instead?

Read the experiment described in the "What steps will reproduce the bug?" section

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-bugIssues with confirmed bugs.streamIssues and PRs related to the stream subsystem.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions