gcx/backend/packages/common/src/health/graceful-shutdown.service.ts

66 lines
2.0 KiB
TypeScript

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'})`);
}
}