execaのexecaSyncで痛い目にあった話
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されるので、そのループから抜けるまではメイン(?)のループに戻らない、みたいな感じでしょうか。
Discussion