Skip to content

【TypeScript】【Express】 expressに graceful shutdownを設定する

はじめに

この記事では、Expressアプリケーションに graceful shutdown(優雅な終了処理)を設定する方法について手を動かして勉強します。

実装

3秒後にレスポンスを返す、エンドポイントを用意しました。

import express, { Application, Request, Response } from "express";
const app: Application = express();
const PORT = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
app.post(
"/",
async (
{ body: { id } }: Request<{}, {}, { id: number }, {}>,
res: Response
) => {
console.log("requestID", id)
await sleep(3000);
return res.status(200).send({
response: id,
});
}
);
const server = app.listen(PORT, () => {
console.log(`dev server running at: http://localhost:${PORT}/`);
});
process.on("SIGINT", () => {
server.close(() => {
console.log("Process terminated.");
});
});
process.on("SIGTERM", () => {
server.close(() => {
console.log("Process terminated.");
});
});

クライアント側からは、以下のcurlを実行することで、3秒後にレスポンスを取得できます。

Terminal window
$ curl -X POST http://localhost:3000 -d "id=1"
{"response":"1"}

リクエストの途中でサーバを落としてみる

forループで10回リクエストを投げます。

Terminal window
$ for i in {1..10}; do curl -X POST http://localhost:3000 -d "id=${i}"; echo ; done

まずはじめにgraceful shutdownをしないで実行します。

import express, { Application, Request, Response } from "express";
const app: Application = express();
const PORT = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
app.post(
"/",
async (
{ body: { id } }: Request<{}, {}, { id: number }, {}>,
res: Response
) => {
console.log("requestID", id)
await sleep(3000);
return res.status(200).send({
response: id,
});
}
);
const server = app.listen(PORT, () => {
console.log(`dev server running at: http://localhost:${PORT}/`);
});
// 以下をコメントアウト
// process.on("SIGINT", () => {
// server.close(() => {
// console.log("Process terminated.");
// });
// });
// process.on("SIGTERM", () => {
// server.close(() => {
// console.log("Process terminated.");
// });
// });

ターミナルの左側でexpressを実行し、右側でリクエストを投げてみます。

expressをrequestID: 3が出たあと、サーバを停止します。
右側でcurlを実行してた方は、レスポンスを待たずにEmpty Reploy from serverが返ってきました。 express-not-graceful-shutdown

続いて、graceful shutdownのコードで実行してみます。

import express, { Application, Request, Response } from "express";
const app: Application = express();
const PORT = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
app.post(
"/",
async (
{ body: { id } }: Request<{}, {}, { id: number }, {}>,
res: Response
) => {
console.log("requestID", id)
await sleep(3000);
return res.status(200).send({
response: id,
});
}
);
const server = app.listen(PORT, () => {
console.log(`dev server running at: http://localhost:${PORT}/`);
});
process.on("SIGINT", () => {
server.close(() => {
console.log("Process terminated.");
});
});
process.on("SIGTERM", () => {
server.close(() => {
console.log("Process terminated.");
});
});

今度はrequestID 3の後にサーバを落としても、curlでリクエストした方は、{"response": "3"}が返ってきました。 express-graceful-shutdown

closeの仕様を確認する

server.close()を呼んでいますが、closeはどういった仕様なのでしょうか?

型定義のコメントを読むと、以下のことがわかります。

  • closeが呼び出されると、新規コネクションの受付を停止する
  • 既存の接続は維持する
  • すべての接続が終了すると、サーバが終了し、‘close’イベントを発生させる
  • callbackは’close’イベントが発生した時に一度だけ実行される
  • callbackの’Error’は、サーバをcloseするとき、openでない場合に発生する

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9a6a0cdd493eecf71c2d5d428d3b7ba28574af70/types/node/net.d.ts#L525-L534

実装はこちら https://github.com/nodejs/node/blob/c311dc43cd2579ecb3c9f86a802899e6aa163dae/lib/net.js#L2147-L2184

つまり、先ほどの話では、requestID = 3 は実行中だったのでレスポンスを返したが、requestID = 4 以降は新規コネクションだったためエラーになったということですね。

今回使ったコードはblog-code/2023/04/express-graceful-shutdown/に置いています。

補足

process.on(event, listener)のeventにはNodeJS.Signalsを指定できます。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9a6a0cdd493eecf71c2d5d428d3b7ba28574af70/types/node/process.d.ts#L53-L90

Expressのapp.listen()から作成したserverは、nodeのhttp.Serverを作成しています。

const server = app.listen(PORT, () => {
console.log(`dev server running at: http://localhost:${PORT}/`);
});
import { TcpSocketConnectOpts, Socket, Server as NetServer, LookupFunction } from 'node:net';
class Server<
Request extends typeof IncomingMessage = typeof IncomingMessage,
Response extends typeof ServerResponse = typeof ServerResponse,
> extends NetServer {

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9a6a0cdd493eecf71c2d5d428d3b7ba28574af70/types/node/http.d.ts#L235-L238

types/node/net.d.tsに書いてあるServerクラス https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9a6a0cdd493eecf71c2d5d428d3b7ba28574af70/types/node/net.d.ts#L472

参考