[Regression on closed #549/#328] main.ts still never calls app.enableShutdownHooks(), so SIGTERM skips all lifecycle cleanup

オープン
#747 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
58/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
node.js, postgresql, typescript

調査の方向性

Start with src/main.ts and src/main.spec.ts, which the issue identifies as the bootstrap and existing test entry point. Read how the app mock is set up, then verify shutdown hooks are enabled before listen() and covered by the test. The issue also proposes deployment documentation about the termination grace period; done means hooks run on process signals and the test protects against their removal.

索引モデルが issue の本文から書いたものです。

説明

bug

Location

src/main.ts:12-74 — bootstrap(). grep -rn "enableShutdownHooks" src/ returns nothing.

Problem

Nest does not listen for process termination signals unless the application explicitly opts in with app.enableShutdownHooks(). Without that call, SIGTERM/SIGINT terminate the process directly: onModuleDestroy, beforeApplicationShutdown and onApplicationShutdown never run on any provider, and app.close() is never invoked.

This is closed issue #328, re-filed and closed again as #549. The call is still absent.

Why it matters

This application has three kinds of state that need an orderly stop, and none of them get one:

1. In-flight money movement. SorobanClientService.pollTransaction polls a submitted transaction for up to roughly 60 seconds (see the discussion in src/common/idempotency/idempotency.interceptor.ts:37-52). A pod killed mid-poll loses the polling loop while the transaction is already on the network. The Escrow row is left in whatever interim state it held, and the IdempotencyKey row stays PROCESSING until STALE_PROCESSING_MS (180s) elapses and a retry reclaims it — at which point the retry may re-submit a mutation whose first attempt actually landed on-chain. A graceful shutdown that drains in-flight requests before closing is precisely what keeps that window shut.

2. Scheduled jobs. ScheduleModule.forRoot() is registered (app.module.ts:39) and @Cron jobs exist for bounty expiry and idempotency cleanup. ScheduleModule cancels its timers in onApplicationShutdown. Without the hook, a job that fires during the termination grace period runs against a connection pool that is about to vanish.

3. The database connection pool. TypeOrmModule closes its pool in onApplicationShutdown. Skipping it leaves Postgres to reap the abandoned backends on its own timeout. Across a rolling deploy or a crash-loop this accumulates idle connections and can exhaust max_connections — which then fails the new pods' startup, turning a routine restart into an outage.

Containers and orchestrators all stop processes with SIGTERM (docker stop, a Kubernetes pod deletion, a systemd restart), so this affects every single deploy and scale-down, not an edge case.

Reproduction / failure path

  1. Start the app against a real Postgres: npm run start:prod.
  2. In a second shell, watch the server's connections:
SELECT count(*) FROM pg_stat_activity WHERE application_name LIKE '%node%' OR datname = 'mergefi';
  1. Send SIGTERM to the Node process (kill <pid>, not kill -9).
  2. Observed: the process exits immediately with no shutdown logging, and the Postgres backends remain in idle state until the server's own tcp_keepalives/idle_session_timeout reaps them. Nest's onApplicationShutdown hooks never fire — add a provider with an onApplicationShutdown() that logs, and confirm the line is never printed.
  3. Expected: the hooks fire, the pool is closed, the scheduler's timers are cancelled, and the process exits after cleanup.

Suggested fix

Call it before listen() so the signal handlers are installed before the process can start accepting traffic:

const app = await NestFactory.create(AppModule, { rawBody: true });
app.enableShutdownHooks();

Pair it with a request-draining grace period so in-flight escrow mutations are allowed to complete rather than cut off — Nest's app.close() stops accepting new connections and waits for the HTTP server to finish current requests, which is what the hook enables.

Because this has now been removed twice, pin it with a test. src/main.spec.ts already mocks NestFactory.create; assert on the returned app mock:

expect(appMock.enableShutdownHooks).toHaveBeenCalled();

Also add a HEALTHCHECK-adjacent note to the deployment docs: with the hooks enabled, the orchestrator's terminationGracePeriodSeconds must exceed the Soroban poll ceiling (~60s) for the drain to be useful, otherwise the pod is still force-killed mid-poll.

主要言語
TypeScript
スター
2
フォーク
90
平均マージ
31分
マージ済み PR(30日)
20

環境構築

  • Dockerfile または Docker Compose ファイルあり
  • プルリクエストのテンプレートなし
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

MergeFi/backend のほかの issue

MergeFi/backend の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。