はじめに
Node.jsのストリーム書き込み使いますか?私は最近Glueジョブの検証で、Firehoseから出力された大量テキストデータを用意する必要があり、使いました。Firehoseを経由して作っても良いのですが、お金がかかるので、ローカルで1つのテキストファイルを用意して、split
とgzip
コマンドで用意しました。
このローカルで1つのテキストファイルを作る際に、以下のエラーが出ました。対処法が検索しても見つからなかったの書くことにしました。
SystemError [ERR_SYSTEM_ERROR]: A system error occurred: undefined returned undefined (undefined)
本エラーは、Node.jsでシステムレベルでエラーが発生しており、具体的な原因はエラーメッセージがないため推測に基づく点とエラーの内容はコードを実行するPCのスペックに大きく左右される点ご容赦頂けたらと思います。
環境
要素 | 内容 | 補足 |
---|---|---|
Node.js | v18.18.2 | 20系だとts-nodeでESM形式がうまく動作しなかったため |
その他気になる点は、ソースコードを参照ください。
コード
エラーが出たコード
import * as fs from 'fs';
import { DateTime } from 'luxon';
const DIST_PATH = 'dist';
const CONTENT_LENGTH = 500;
const CONTENT = `${'a'.repeat(CONTENT_LENGTH)}\n}`;
const WRITE_NUM = 5_000_000;
const main = async () => {
if (!fs.existsSync(DIST_PATH)) {
fs.mkdirSync(DIST_PATH);
}
const now = DateTime.now();
const exportPath = `${DIST_PATH}/${now.toFormat('yyyyMMdd-HHmm')}.txt`;
const ws = fs.createWriteStream(exportPath);
for (let i = 0; i < WRITE_NUM; i++) {
ws.write(CONTENT);
drawString(`${i}/${WRITE_NUM}`);
}
ws.end();
};
const clearCurrentLine = (): void => {
process.stdout.write('\r\x1b[2K');
};
const drawString = (str: string): void => {
clearCurrentLine();
process.stdout.write(str);
};
await main();
エラー内容
npx ts-node ./src/index.mts
4999999/5000000SystemError [ERR_SYSTEM_ERROR]: A system error occurred: undefined returned undefined (undefined)
at new SystemError (node:internal/errors:256:5)
at new NodeError (node:internal/errors:367:7)
at node:internal/fs/streams:446:10
at FSReqCallback.wrapper [as oncomplete] (node:fs:955:5) {
code: 'ERR_SYSTEM_ERROR',
info: 'writev failed',
errno: [Getter/Setter],
syscall: [Getter/Setter]
}
解消法
- ws.write(CONTENT);
+ if (!ws.write(CONTENT)) {
+ await new Promise((resolve) => ws.once('drain', resolve));
+ }
drawString(`${i}/${WRITE_NUM}`);
解説
バックプレッシャー
ストリーム処理において、データの生産側(今回だとサンプルデータの生成)とデータの消費側(今回だとファイルの書き込み)で、生産者が消費者よりも速くデータを生成する状況では、消費者が受け取ったデータを処理しきれずにバッファが溢れる恐れがあります。
データの生成者(プロデューサー)と消費者(コンシューマー)の間でデータの流れを調整する機構をバックプレッシャーと呼びます。
Node.jsのストリームとバックプレッシャー
Node.jsはストリームが自動的にバックプレッシャーを管理しています。ws.write
メソッドでfalse
が返却された場合、バッファが一杯であることを示しています。正常に動作するコードは、その間はデータの生産を止めます。消費側が再びデータの書き込みを受け入れる準備ができた時点でdrain
イベントが発生します。このイベントが発生したら書き込みを再開するという形です。
システムエラーの原因は、エラーが起きたコードと正常に動作するコードを比較すると、消費者側が受け取ったデータを処理しきれず、バッファが溢れシステムエラーになったものと推測できます。
さいごに
同期書き込みより圧倒的に速度が出ますし、大量データを読み込みながら書き込むケースでもメモリ効率が良いので是非試して頂ければと思います!
参考
- Node.js Stream を使ってみる
- Streamの詳細解説があり、理解に役立ちました!