🐈

execaのexecaSyncで痛い目にあった話

2022/02/17に公開

execaのexecaSync関数で実行したコマンドが長時間返ってこず、その間Webアプリケーションが一切のリクエストを受け付けなくなりました。

問題点

execaSync実行中はNodeの動作が一時的に停止します。
Webアプリケーションであれば、この間は一切のリクエストを受け付けません。

仕組み

execaSyncは内部ではchild_process.spawnSyncを呼び出しています[1]
spawnSyncはNodeのイベントループを停止しますので、アプリケーションがリクエストを受け付けない状態に陥ります。

The child_process.spawnSync(), child_process.execSync(), and child_process.execFileSync() methods are synchronous and will block the Node.js event loop, pausing execution of any additional code until the spawned process exits.
https://nodejs.org/docs/latest-v16.x/api/child_process.html#synchronous-process-creation

解決策

execa関数に差し替えるだけで解決します。

execaSync(cmd, args);
↓
await execa(cmd, args);

execaは内部ではchild_process.spawnを呼び出しており[2]、execaSync/spawnSyncとは違ってイベントループを停止することはありません。

実験

Node v16.13.0で実験します。

import http from 'http';
import { execa, execaSync } from 'execa';

const server = http.createServer(async (req, res) => {
  switch (req.url) {
    case '/sync':
      execaSync('/bin/sleep', ['10']);
      break;
    case '/async':
      await execa('/bin/sleep', ['10']);
      break;
  }
  res.end();
});

server.listen(8000);

http://localhost:8000/sync のリクエストを処理している間は、他の一切のリクエストを受け付けません。
一方、 http://localhost:8000/async のリクエストを処理している間は、他のリクエストを受け付けます。

まだわかってないこと

child_process.spawnSyncでイベントループが停止する仕組みを調べ中。
libuvのイベントループが新規作成されてuv_runされるので、そのループから抜けるまではメイン(?)のループに戻らない、みたいな感じでしょうか。
https://github.com/nodejs/node/blob/v16.13.0/src/spawn_sync.cc#L517

脚注
  1. https://github.com/sindresorhus/execa/blob/fa9cc053/index.js#L173 ↩︎

  2. https://github.com/sindresorhus/execa/blob/fa9cc053/index.js#L84 ↩︎

Discussion