EggJS 异步获取流时可能导致进程堵塞

在 eggjs 中异步向其它服务请求文件流时,出现异步始终等待,导致请求阻塞问题。通过排查,发现是由于 await 请求后,流无法被消费,流没有消费完,请求会继续等待,从而陷入了逻辑循环。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const { PassThrough,pipeline } = require('stream')
const fs = require('fs')

async convert_html_to_pdf(template, content, resultFileName, printOptions = {}) {
const writeStream = new PassThrough()

await this.curlPython(`/api/html/pdf-stream`, {
writeStream,
method: 'POST',
timeout: 30000,
data: {
template,
html: content,
filename: resultFileName,
printOptions
}
})

return writeStream
}

// 其它文件中使用
const fileStream = await convert_html_to_pdf(...params)
const writeStream = fs.createWriteStream(tempFileFullPath)
pipeline(fileStream, writeStream)

其中, this.curlPython 是对 this.curl 的封装,在此处可以直接理解成 this.curl

该方法的作用是请求 python 服务将 html 转换成 pdf,对方返回一个文件流。代码中使用 PassThrough 中转流到其它地方进行消费。

问题分析

上述代码在本机环境表现一切正常,但是在生产环境总是发生阻塞。

通过查看 eggjs 的 curl 的参数介绍,发现其中提到:

1
2
3
4
5
6
7
8
9
10
export interface RequestOptions {
/**
* A writable stream to be piped by the response stream.
* Responding data will be write to this stream and callback
* will be called with data set null after finished writing.
*/
writeStream?: Writable;
/** consume the writeStream, invoke the callback after writeStream close. */
consumeWriteStream?: boolean;
}

当流结束之后,才会以一个 null data 进行回调

所以这就导致在本文开头所讲的情况:

await 请求后,流无法被消费,流没有消费完,请求会继续等待,从而陷入了逻辑循环。

所以,遇到这种情况时,只需要去掉 await 直接返回 PassThrough 让程序对流进行消费即可,即代码为:

1
2
3
4
5
6
7
8
9
10
11
await this.curlPython(`/api/html/pdf-stream`, {
writeStream,
method: 'POST',
timeout: 30000,
data: {
template,
html: content,
filename: resultFileName,
printOptions
}
})

残留问题

为什么在本机测试时是正常,但是到生产环境总是发生阻塞呢?这个问题我也一直没想明白。

通过问 AI,猜测可能原因是:python 服务器的响应是流式的,但没有正确发送结束信号(EOF),writeStream 可能不会触发 'close' 事件,导致 Promise 永不 resolve,从而 await 阻塞,无法执行后续代码。

欢迎大家在下面留言讨论,提前为大家的解惑致以感谢!