Graceful Shutdownとは
Graceful Shutdownとは、実行中のアプリケーションを安全に終了させることを意味します。Cloud Foundryでは、終了のシグナル(例:SIGTERM)を検知したアプリケーションは、実行中の処理を終了させたり、新規のリクエストの受付を停止したりする必要があります。
CAPにおけるGraceful Shutdown
CAP Javaの場合、Cloud Foundryで実行中のアプリケーションを落とすと自動的にGraceful Shutdownが行われていました。
このことから、Node.jsでも当然Graceful Shutdownが行われるものと思っていましたが、気になる記事に出会ってしまいました。
Tips for running CAP nodejs in SAP BTP
記事によればNode.jsで起動コマンドをnpm startにしている場合、CAPのアプリケーションは子プロセスで起動するためコンテナの終了シグナルを検知することができず、Graceful Shutdownができないということです。
起動コマンドはmta.yamlで明示しなければ自動的にnpm startになるため、デフォルトではGraceful Shutdownができない状態になっているということです。
結論
検証の結果わかったことは以下です。
- 起動コマンドを指定しなくても(npm startでも)、トランザクション整合性は担保される
- CAPのトランザクション整合性は終了シグナルに反応しているわけではなく、処理の最後にコミットしているため担保できていると推測
- 終了シグナルをキャッチして独自の終了処理を行いたい場合は起動コマンドを指定する必要がある
検証
指定された件数だけSales Orderを作成するアクションを作ります。DBにSales Orderデータを格納後、1件ごとに10秒のwaitを入れます。処理の途中でサービスを再起動した場合に何が起こるかを観察します。
CAPプロジェクト
ソースコードは以下に格納しています。
https://github.com/miyasuta/cap-nodejs-gracefulshutdown
スキーマ定義
namespace outbox;
using { managed } from '@sap/cds/common';
entity SalesOrders: managed {
key ID: UUID;
customerId: Integer;
orderDate: Date;
amount: Integer;
}
サービス定義
using { outbox as db } from '../db/schema';
service SalesService {
entity SalesOrders as projection on db.SalesOrders;
action createBulkOrders(count: Integer) returns String;
}
イベントハンドラ定義
import cds from '@sap/cds'
import { SalesOrders, SalesOrder, createBulkOrders } from '#cds-models/SalesService'
import { CdsDate } from '#cds-models/_'
const LOG = cds.log('cli')
LOG.info('debug enabled?: ', LOG._debug)
// Helper to format Date as 'yyyy-mm-dd'
function formatDate(date: Date): CdsDate {
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}` as CdsDate;
}
export class SalesService extends cds.ApplicationService { init() {
this.on (createBulkOrders, async (req) => {
// delete exsisting sales orders
const db = await cds.connect.to('db')
const { SalesOrders } = db.entities('outbox')
await DELETE .from (SalesOrders)
let { count } = req.data
count = Number(count)
// for count times, create a sales order
let salesOrder: SalesOrder
for (let i = 0; i < count; i++) {
salesOrder = {
customerId: i,
orderDate: formatDate(new Date()),
amount: 100 * i,
}
await INSERT .into (SalesOrders) .entries (salesOrder)
LOG.info(`${i + 1} 件目の受注を登録しました`)
LOG.debug(`${i + 1} 件目の受注を登録しました(デバッグ用)`)
// wait 10 seconds
await new Promise(resolve => setTimeout(resolve, 10000))
}
return `Created ${count} sales orders successfully.`
})
return super.init()
}}
終了シグナルをキャッチする処理
ブログを参考に、srv/server.jsに終了シグナルをキャッチする処理を入れます。
import cds from '@sap/cds';
process.on('SIGINT', () => {
console.log('SIGINT signal received: shutting down...');
});
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: shutting down...');
});
export = cds.server;
実験
①起動コマンドを指定しない場合
mta.yamlに起動コマンドを指定せずにデプロイします。
modules:
- name: cap-nodejs-gracefulshutdown-srv
type: nodejs
path: gen/srv
parameters:
instances: 1
buildpack: nodejs_buildpack
memory: 256M
10件のSales Orderを登録するリクエストを送り、直後にcf stopでアプリケーションを停止します。
### createBulkOrders
# @name createBulkOrders_POST
POST {{server}}/odata/v4/sales/createBulkOrders
Content-Type: application/json
{
"count": 10
}
502 (Bad Gateway) エラーが返ってきました。DBにSales Orderは登録されていませんでした。よって、トランザクション整合性は確保されています。

ログを見ると7件目のSales Orderを登録した後(約60秒経過後)にコンテナが削除されたことがわかります。終了シグナルをキャッチしたログは出力されていません。
2025-06-30T20:35:26.38+0000 [APP/PROC/WEB/0] STDOUT {"msg":"1 件目の受注を登録しました(デバッグ用)", ... }
2025-06-30T20:35:29.63+0000 [API/3] STDOUT Stopping app with guid bb8123dd-...
2025-06-30T20:35:29.66+0000 [CELL/0] STDOUT stopping instance c80d0fbc-...
//中略
2025-07-01T05:36:26.46+0900 [APP/PROC/WEB/0] OUT {"msg":"7 件目の受注を登録しました(デバッグ用)", ... }
2025-07-01T05:36:36.21+0900 [CELL/0] OUT destroying container for instance c80d0fbc-...
2025-07-01T05:36:36.22+0900 [RTR/5] OUT response_time:69.88s, endpoint_failure (EOF)
2025-07-01T05:36:36.37+0900 [PROXY/0] OUT Exit status 137
2025-07-01T05:36:37.74+0900 [CELL/0] OUT successfully destroyed container for instance c80d0fbc-...
②起動コマンドを指定した場合
mta.yamlでparameters.commandにcds-serveを指定します。これはpackage.jsonの"start"スクリプトに設定されているコマンドと同じです。
modules:
- name: cap-nodejs-gracefulshutdown-srv
type: nodejs
path: gen/srv
parameters:
instances: 1
buildpack: nodejs_buildpack
memory: 256M
command: cds-serve # 追加
一度commandを指定すると、mta.yamlでコマンドを削除したり変更してもコマンドは書き変わりません。cf undeployするか、mta.yamlのバージョンを上げる必要があります。
10件のSales Orderを登録するリクエストを送り、直後にcf stopでアプリケーションを停止します。結果は①と同じ502エラーおよびDBに登録なしですが、終了シグナルをキャッチしたログ(SIGTERM signal received: shutting down...)が出力されています。さらに、①のときにはなかったexceeded 1m0s graceful shutdown intervalというメッセージが出力されています。よって、起動コマンドを指定した場合はGraceful Shutdownの処理ができていると確認できました。
2025-06-30T20:44:45.12+0000 [APP/PROC/WEB/0] STDOUT {"msg":"1 件目の受注を登録しました(デバッグ用)", ... }
2025-06-30T20:44:48.55+0000 [API/0] STDOUT Stopping app with guid bb8123dd-...
2025-06-30T20:44:48.58+0000 [CELL/0] STDOUT stopping instance 9c812ecf-...
2025-06-30T20:44:53.91+0000 [APP/PROC/WEB/0] STDOUT SIGTERM signal received: shutting down...
//中略
2025-07-01T05:45:45.21+0900 [APP/PROC/WEB/0] OUT {"msg":"7 件目の受注を登録しました(デバッグ用)", ... }
2025-07-01T05:45:53.92+0900 [RTR/3] OUT response_time:68.98s, endpoint_failure (EOF)
2025-07-01T05:45:53.92+0900 [APP/PROC/WEB/0] OUT Exit status 137 (exceeded 1m0s graceful shutdown interval)
2025-07-01T05:45:53.93+0900 [CELL/0] OUT destroying container for instance 9c812ecf-...
2025-07-01T05:45:54.03+0900 [PROXY/0] OUT Exit status 137
2025-07-01T05:45:55.20+0900 [CELL/0] OUT successfully destroyed container for instance 9c812ecf-...