stateless な per-request サーバー生成にプロセス寿命の副作用(signal ハンドラ等)を混ぜない
Node.js
MCP
信頼性
知識
運用
常駐プロセス前提(stdio サーバーや CLI)で書かれた初期化コードを、リクエスト毎にサーバーインスタンスを生成する stateless HTTP 構成(MCP の StreamableHTTP など)に移植すると、初期化内のプロセスレベル副作用(process への signal ハンドラ登録、グローバルなイベント購読、atexit 相当の登録)がリクエスト毎に実行されて蓄積する。インスタンスの close ではプロセスレベル登録は外れないため、リスナーとクロージャがプロセス寿命の間リークし、Node.js では数十リクエストで MaxListenersExceededWarning、signal 受信時には登録数分の終了処理が多重発火する。
発生条件
- 「1プロセス = 1サーバー」の前提で書かれた setup 関数(エラーハンドリング設定・graceful shutdown 登録)を、ファクトリ化したサーバー生成関数の中にそのまま持ち込んだときに起きる。移植・リファクタ時に見落としやすい。
対処と予防
- 副作用をプロセス寿命(signal・unhandledRejection 等の登録:起動時に1回だけ)とリクエスト寿命(サーバーインスタンスに閉じる資源:close で解放)に分類し、前者は entrypoint の bootstrap にだけ置く。ファクトリ関数にはプロセスレベu30eb副作用を持たせない。
- 移植レビューでは「この関数はプロセスあたり何回呼ばれるか」を初期化系の関数ごとに問うと検出できる。
検証
テストでリクエストを複数回流した後に process の対象 signal のリスナー数が増加していないことを確認する(listenerCount の前後比較)。警告の発生を待たずに検出できる。