223 lines
5.8 KiB
Markdown
223 lines
5.8 KiB
Markdown
---
|
|
title: Implement Graceful Shutdown
|
|
impact: MEDIUM-HIGH
|
|
impactDescription: Proper shutdown handling ensures zero-downtime deployments
|
|
tags: devops, graceful-shutdown, lifecycle, kubernetes
|
|
---
|
|
|
|
## Implement Graceful Shutdown
|
|
|
|
Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.
|
|
|
|
**Incorrect (ignoring shutdown signals):**
|
|
|
|
```typescript
|
|
// Ignore shutdown signals
|
|
async function bootstrap() {
|
|
const app = await NestFactory.create(AppModule);
|
|
await app.listen(3000);
|
|
// App crashes immediately on SIGTERM
|
|
// In-flight requests fail
|
|
// Database connections are abruptly closed
|
|
}
|
|
|
|
// Long-running tasks without cancellation
|
|
@Injectable()
|
|
export class ProcessingService {
|
|
async processLargeFile(file: File): Promise<void> {
|
|
// No way to interrupt this during shutdown
|
|
for (let i = 0; i < file.chunks.length; i++) {
|
|
await this.processChunk(file.chunks[i]);
|
|
// May run for minutes, blocking shutdown
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Correct (enable shutdown hooks and handle cleanup):**
|
|
|
|
```typescript
|
|
// Enable shutdown hooks in main.ts
|
|
async function bootstrap() {
|
|
const app = await NestFactory.create(AppModule);
|
|
|
|
// Enable shutdown hooks
|
|
app.enableShutdownHooks();
|
|
|
|
// Optional: Add timeout for forced shutdown
|
|
const server = await app.listen(3000);
|
|
server.setTimeout(30000); // 30 second timeout
|
|
|
|
// Handle graceful shutdown
|
|
const signals = ['SIGTERM', 'SIGINT'];
|
|
signals.forEach((signal) => {
|
|
process.on(signal, async () => {
|
|
console.log(`Received ${signal}, starting graceful shutdown...`);
|
|
|
|
// Stop accepting new connections
|
|
server.close(async () => {
|
|
console.log('HTTP server closed');
|
|
await app.close();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Force exit after timeout
|
|
setTimeout(() => {
|
|
console.error('Forced shutdown after timeout');
|
|
process.exit(1);
|
|
}, 30000);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Lifecycle hooks for cleanup
|
|
@Injectable()
|
|
export class DatabaseService implements OnApplicationShutdown {
|
|
private readonly connections: Connection[] = [];
|
|
|
|
async onApplicationShutdown(signal?: string): Promise<void> {
|
|
console.log(`Database service shutting down on ${signal}`);
|
|
|
|
// Close all connections gracefully
|
|
await Promise.all(
|
|
this.connections.map((conn) => conn.close()),
|
|
);
|
|
|
|
console.log('All database connections closed');
|
|
}
|
|
}
|
|
|
|
// Queue processor with graceful shutdown
|
|
@Injectable()
|
|
export class QueueService implements OnApplicationShutdown, OnModuleDestroy {
|
|
private isShuttingDown = false;
|
|
|
|
onModuleDestroy(): void {
|
|
this.isShuttingDown = true;
|
|
}
|
|
|
|
async onApplicationShutdown(): Promise<void> {
|
|
// Wait for current jobs to complete
|
|
await this.queue.close();
|
|
}
|
|
|
|
async processJob(job: Job): Promise<void> {
|
|
if (this.isShuttingDown) {
|
|
throw new Error('Service is shutting down');
|
|
}
|
|
await this.doWork(job);
|
|
}
|
|
}
|
|
|
|
// WebSocket gateway cleanup
|
|
@WebSocketGateway()
|
|
export class EventsGateway implements OnApplicationShutdown {
|
|
@WebSocketServer()
|
|
server: Server;
|
|
|
|
async onApplicationShutdown(): Promise<void> {
|
|
// Notify all connected clients
|
|
this.server.emit('shutdown', { message: 'Server is shutting down' });
|
|
|
|
// Close all connections
|
|
this.server.disconnectSockets();
|
|
}
|
|
}
|
|
|
|
// Health check integration
|
|
@Injectable()
|
|
export class ShutdownService {
|
|
private isShuttingDown = false;
|
|
|
|
startShutdown(): void {
|
|
this.isShuttingDown = true;
|
|
}
|
|
|
|
isShutdown(): boolean {
|
|
return this.isShuttingDown;
|
|
}
|
|
}
|
|
|
|
@Controller('health')
|
|
export class HealthController {
|
|
constructor(private shutdownService: ShutdownService) {}
|
|
|
|
@Get('ready')
|
|
@HealthCheck()
|
|
readiness(): Promise<HealthCheckResult> {
|
|
// Return 503 during shutdown - k8s stops sending traffic
|
|
if (this.shutdownService.isShutdown()) {
|
|
throw new ServiceUnavailableException('Shutting down');
|
|
}
|
|
|
|
return this.health.check([
|
|
() => this.db.pingCheck('database'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Integrate with shutdown
|
|
@Injectable()
|
|
export class AppShutdownService implements OnApplicationShutdown {
|
|
constructor(private shutdownService: ShutdownService) {}
|
|
|
|
async onApplicationShutdown(): Promise<void> {
|
|
// Mark as unhealthy first
|
|
this.shutdownService.startShutdown();
|
|
|
|
// Wait for k8s to update endpoints
|
|
await this.sleep(5000);
|
|
|
|
// Then proceed with cleanup
|
|
}
|
|
}
|
|
|
|
// Request tracking for in-flight requests
|
|
@Injectable()
|
|
export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
|
|
private activeRequests = 0;
|
|
private isShuttingDown = false;
|
|
private shutdownPromise: Promise<void> | null = null;
|
|
private resolveShutdown: (() => void) | null = null;
|
|
|
|
use(req: Request, res: Response, next: NextFunction): void {
|
|
if (this.isShuttingDown) {
|
|
res.status(503).send('Service Unavailable');
|
|
return;
|
|
}
|
|
|
|
this.activeRequests++;
|
|
|
|
res.on('finish', () => {
|
|
this.activeRequests--;
|
|
if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {
|
|
this.resolveShutdown();
|
|
}
|
|
});
|
|
|
|
next();
|
|
}
|
|
|
|
async onApplicationShutdown(): Promise<void> {
|
|
this.isShuttingDown = true;
|
|
|
|
if (this.activeRequests > 0) {
|
|
console.log(`Waiting for ${this.activeRequests} requests to complete`);
|
|
this.shutdownPromise = new Promise((resolve) => {
|
|
this.resolveShutdown = resolve;
|
|
});
|
|
|
|
// Wait with timeout
|
|
await Promise.race([
|
|
this.shutdownPromise,
|
|
new Promise((resolve) => setTimeout(resolve, 30000)),
|
|
]);
|
|
}
|
|
|
|
console.log('All requests completed');
|
|
}
|
|
}
|
|
```
|
|
|
|
Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)
|