Node.jsでAbortControllerを使って、child_processを強制的に終了させる方法

はいさい！ちゅらデータぬオースティンやいびーん！

# 概要
AbortControllerを使って、Node.jsで実行した`child_process`を強制的に終了させる方法を紹介します。

## 背景
本記事は、前回投稿した、Node.jsでffmpegを使って動画を変換する記事を元に作成しています。

変換の作業に時間がかかりすぎて、サーバーの負荷が過ぎないように、ある一定の時間が経っても子プロセスが終了しなかったら、強制的に終了させる必要があります。

ここで、Abort Controllerを使ってShellに終了のシグナルを送ることができますので、解説していきたいと思います。

## 事前知識
前回の記事を読んでいただければわかりやすいかと思います！

https://qiita.com/tronicboy/items/f3f5d2dbcade2a8e01d9

# コード
前回のコードのPOSTのところを修正します。

`spawn`の引数の設定オブジェクトに、`signal`を追加します。
```typescript:src/index.ts
...

app.post("/", upload.single("file"), async (req, res) => {
  const controller = new AbortController();

  try {
    if (!req.file) throw Error("A file must be provided.");

    const filename = req.file.filename;
    const filenameWithoutExtension = filename.split(".")[0];
    const result = await new Promise<string>((resolve, reject) => {
      const childProcess = spawn(
        `ffmpeg -i uploads/${filename} -c:v libx264 -preset veryfast -r 30 -vf "scale=720:-1" -c:a copy converted/${filenameWithoutExtension}.mp4`,
        {
          shell: true,
          signal: controller.signal // ここで、AbortControllerのsignalを渡します！
        }
      );

      childProcess.on("exit", (code, signal) => {
        if (code !== 0) {
          reject(`Child process failed with exit code: ${code}`);
        }
        resolve(`${filenameWithoutExtension}.mp4`);
      });
    });

    res.statusCode = 200;
    res.send(result);
  } catch (error) {
    res.statusCode = 500;
    res.send(error);
  }
});

...
```

### setTimeoutでAbortController.abortを実行してみる
最初に、単純に`setTimeout`で`controller.abort`を実行してみて、どうなるかみてみましょう。

先ほどのPromiseの後に、以下のコードを追加します。
```typescript
...

    setTimeout(() => controller.abort(), 10);

...
```
すると、変換が`1`で失敗します。
![スクリーンショット 2022-07-13 13.26.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/5a0403cb-4f22-e307-3a91-726dcea8f529.png)

これだけでできます！

## Promise.raceで終了させる方法
Promise.raceで`setTimeout`を包むと、エラーメッセージを設定することができます。
```typescript
...

app.post("/", upload.single("file"), async (req, res) => {
  const controller = new AbortController();

  try {
    if (!req.file) throw Error("A file must be provided.");

    const filename = req.file.filename;
    const filenameWithoutExtension = filename.split(".")[0];
    const result = await Promise.race<string>([
      new Promise((resolve, reject) => {
        const childProcess = spawn(
          `ffmpeg -i uploads/${filename} -c:v libx264 -preset veryfast -r 30 -vf "scale=720:-1" -c:a copy converted/${filenameWithoutExtension}.mp4`,
          {
            shell: true,
            signal: controller.signal,
          }
        );

        childProcess.on("exit", (code, signal) => {
          if (code !== 0) {
            reject(`Child process failed with exit code: ${code}`);
          }
          resolve(`${filenameWithoutExtension}.mp4`);
        });
      }),
      new Promise((_, reject) => setTimeout(() => reject("Process exceeded time limit."), 1000)),
    ]);

    res.statusCode = 200;
    res.send(result);
  } catch (error) {
    res.statusCode = 500;
    res.send(error);
  }
});

...
```
こうすると、上記のように変換のプロセスがタイムアウトした時に、以下のようなエラーが出ます。
![スクリーンショット 2022-07-13 13.35.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/54b5c25d-24b1-5adc-6cce-87f70c00ca18.png)

# まとめ
ここまでAbortControllerを使って、child_processを外部から強制的に終了させる方法を紹介しましたが、いかがでしょうか？

本記事の他に、多数のAbortController関連の記事を書いていますが、読んでいただければ分かるように、AbortControllerは非常に便利です。

何かを止めたい！と考えたら、まずはAbortControllerでできないか考えるようにしましょう！