[小ネタ] Node.jsのストリーム書き込みでSystemErrorが出た場合の対処法

2024.02.08

はじめに

Node.jsのストリーム書き込み使いますか?私は最近Glueジョブの検証で、Firehoseから出力された大量テキストデータを用意する必要があり、使いました。Firehoseを経由して作っても良いのですが、お金がかかるので、ローカルで1つのテキストファイルを用意して、splitgzipコマンドで用意しました。

このローカルで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イベントが発生します。このイベントが発生したら書き込みを再開するという形です。

システムエラーの原因は、エラーが起きたコードと正常に動作するコードを比較すると、消費者側が受け取ったデータを処理しきれず、バッファが溢れシステムエラーになったものと推測できます。

さいごに

同期書き込みより圧倒的に速度が出ますし、大量データを読み込みながら書き込むケースでもメモリ効率が良いので是非試して頂ければと思います!

参考