节点:管道在暂停的直通时不会阻塞

时间:2019-01-14 14:46:38

标签: node.js asynchronous stream

节点的流的基本行为之一是在写入暂停的流时阻塞,并且任何非管道流都被阻塞。

在此示例中,创建的PassThrough没有通过管道传递到其创建事件循环中的任何内容。有人希望在此PassThrough上运行的所有管道都将阻塞,直到它被管道传输/附加了数据事件为止,但事实并非如此。

pipeline回调,但不消耗任何空间。

const {promises: pFs} = require('fs');
const fs = require('fs');
const {PassThrough} = require('stream');
const {pipeline: pipelineCb} = require('stream');
const util = require('util');
const pipeline = util.promisify(pipelineCb);
const path = require('path');
const assert = require('assert');

/**
 * Start a test ftp server
 * @param {string} outputPath
 * @return {Promise<void>}
 */
function myCreateWritableStream (outputPath) {
    // The stream is created in paused mode -> should block until piped
    const stream = new PassThrough();

    (async () => {
        // Do some stuff (create directory / check space / connect...)
        await new Promise(resolve => setTimeout(resolve, 500));

        console.log('piping passThrough to finale output');

        // Consume the stream
        await pipeline(stream, fs.createWriteStream(outputPath));

        console.log('passThrough stream content written');
    })().catch(e => {
        console.error(e);
        stream.emit('error', e);
    });

    return stream;
}

/**
 * Main test function
 * @return {Promise<void>}
 */
async function main () {
    // Prepare the test directory with a 'tmp1' file only
    const smallFilePath = path.join(__dirname, 'tmp1');
    const smallFileOut = path.join(__dirname, 'tmp2');

    await Promise.all([
        pFs.writeFile(smallFilePath, 'a small content'),
        pFs.unlink(smallFileOut).catch(e => assert(e.code === 'ENOENT'))
    ]);

    // Duplicate the tmp1 file to tmp2
    await pipeline([
        fs.createReadStream(smallFilePath),
        myCreateWritableStream(smallFileOut)
    ]);
    console.log('pipeline ended');

    // Check content
    const finalContent = await pFs.readdir(__dirname);
    console.log('directory content');
    console.log(finalContent.filter(file => file.startsWith('tmp')));
}


main().catch(e => {
    process.exitCode = 1;
    console.error(e);
});

此代码输出以下行:

pipeline ended
directory content
[ 'tmp1' ]
piping passThrough to finale output
passThrough stream content written

如果pipeline确实等待流结束,那么输出将是以下内容:

piping passThrough to finale output
passThrough stream content written
pipeline ended
directory content
[ 'tmp1', 'tmp2' ]

您如何解释这种行为?

1 个答案:

答案 0 :(得分:1)

我认为API不能保证您在此处找到所需的保证。

stream.pipeline在所有数据写完后调用其回调。由于数据已被写入新的Transform流(您的Passthrough),并且该流还没有放置数据的位置,因此只需将其存储在流的内部缓冲区中即可。这对于管道已经足够了。

如果您要读取一个足够大的文件,并填充了Transform流的缓冲区,则stream backpressure可以在正在读取文件的可读内容上自动触发pause()。转换流耗尽后,它将自动unpause()可读,以便恢复数据流。

我认为您的示例有两个错误的假设:

(1)可以暂停转换流。根据{{​​3}}的说法,暂停任何通过管道传输到目标的流都是无效的,因为一旦通过管道传输的目标请求更多数据,该流将立即自动暂停。此外,暂停的转换流仍会读取数据!暂停的流只是不写入数据。

(2)流水线下方的停顿以某种方式传播到流水线的前端,并导致数据停止流动。如果是由背压引起的,则仅是正确的,这意味着您将需要触发节点检测到完整的内部缓冲区。

在使用管道时,最好假定您对两个最远的端部有手动控制,但不一定要控制中间的任何一个。 (您可以手动pipe()unpipe()来连接和断开中间流,但不能暂停它们。)