Files
lcbp3/.agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md
admin ef16817f38
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
260223:1415 20260223 nextJS & nestJS Best pratices
2026-02-23 14:15:06 +07:00

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)