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

Đang mở
#747 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
58/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
node.js, postgresql, typescript
Lĩnh vực
backend, databases, devops

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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.

Ngôn ngữ chính
TypeScript
Star
2
Fork
90
Merge trung bình
31 phút
Pull request đã merge (30 ngày)
20

Chuẩn bị môi trường

  • Có Dockerfile hoặc tệp Docker Compose
  • Không có mẫu pull request
  • Không có hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của MergeFi/backend

Tất cả issue của MergeFi/backend

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.