import { Injectable, Logger, OnApplicationShutdown, BeforeApplicationShutdown, } from '@nestjs/common'; import { HealthController } from './health.controller'; /** * Graceful Shutdown Service - ensures zero-downtime rolling upgrades. * * Shutdown sequence: * 1. Receive SIGTERM (from K8s, docker stop, etc.) * 2. Mark service as NOT ready (health/ready returns 503) * 3. Wait for drain period (allow in-flight requests to complete) * 4. Close connections (DB, Redis, Kafka) * 5. Exit process * * K8s preStop hook should wait ~5s before sending SIGTERM, * giving the load balancer time to remove this pod from rotation. */ @Injectable() export class GracefulShutdownService implements BeforeApplicationShutdown, OnApplicationShutdown { private readonly logger = new Logger('GracefulShutdown'); private readonly drainTimeoutMs: number; constructor(private readonly healthController: HealthController) { this.drainTimeoutMs = parseInt( process.env.GRACEFUL_SHUTDOWN_DRAIN_MS || '10000', 10, ); } /** * Called before application shutdown begins. * Mark as not ready and wait for drain period. */ async beforeApplicationShutdown(signal?: string) { this.logger.warn( `Shutdown signal received: ${signal || 'unknown'}. Starting graceful shutdown...`, ); // Step 1: Mark as not ready (stop accepting new requests) this.healthController.setReady(false); this.logger.log('Marked service as NOT ready'); // Step 2: Wait for drain period (in-flight requests to complete) this.logger.log( `Waiting ${this.drainTimeoutMs}ms for in-flight requests to drain...`, ); await new Promise((resolve) => setTimeout(resolve, this.drainTimeoutMs), ); this.logger.log('Drain period complete'); } /** * Called after application shutdown. Final cleanup logging. */ async onApplicationShutdown(signal?: string) { this.logger.log(`Application shutdown complete (signal: ${signal || 'none'})`); } }