Dowódco legionów! Nawet najpotężniejsze Imperium Rzymskie musiało umieć wycofywać swoje wojska w sposób uporządkowany. Gdy Cezar nakazywał odwrót, każdy centurion wiedział dokładnie co zrobić - zabezpieczyć sprzęt, ewakuować rannych, spalić mosty za sobą. W NestJS graceful shutdown to identyczna sztuka - zamykanie aplikacji w sposób, który nie pozostawia żadnego chaosu za sobą.
Gdy aplikacja NestJS otrzymuje sygnał zamknięcia (np. SIGTERM podczas deploymentu), musi:
NestJS oferuje specjalne hooki cyklu życia do obsługi zamykania. Są wywoływane w określonej kolejności:
1// graceful-shutdown.service.ts
2import {
3 Injectable,
4 OnModuleDestroy,
5 BeforeApplicationShutdown,
6 OnApplicationShutdown,
7 Logger
8} from '@nestjs/common';
9
10@Injectable()
11export class LegionShutdownService
12 implements OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown
13{
14 private readonly logger = new Logger(LegionShutdownService.name);
15 private activeRequests = 0;
16
17 // 1. OnModuleDestroy - pierwszy sygnał do odwrotu
18 // Wywoływany gdy moduł jest niszczony
19 async onModuleDestroy() {
20 this.logger.warn('OnModuleDestroy: Rozpoczynam procedurę odwrotu legionów!');
21 // Zamknij połączenia specyficzne dla modułu
22 }
23
24 // 2. BeforeApplicationShutdown - przygotowanie do zamknięcia
25 // Otrzymuje sygnał (SIGTERM, SIGINT) jako argument
26 async beforeApplicationShutdown(signal?: string) {
27 this.logger.warn(
28 `BeforeApplicationShutdown: Sygnał ${signal} - czekam na dokończenie operacji`
29 );
30
31 // Czekaj aż wszystkie aktywne żądania się zakończą
32 while (this.activeRequests > 0) {
33 this.logger.log(`Pozostało ${this.activeRequests} aktywnych operacji...`);
34 await new Promise(resolve => setTimeout(resolve, 1000));
35 }
36 }
37
38 // 3. OnApplicationShutdown - finalne zamknięcie
39 async onApplicationShutdown(signal?: string) {
40 this.logger.warn(`OnApplicationShutdown: Sygnał ${signal} - zamykam bramy Imperium!`);
41 // Ostateczne czyszczenie zasobów
42 }
43}Domyślnie NestJS nie nasłuchuje na sygnały systemowe. Musisz je aktywować w
main.ts:1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4import { Logger } from '@nestjs/common';
5
6async function bootstrap() {
7 const app = await NestFactory.create(AppModule);
8 const logger = new Logger('Bootstrap');
9
10 // Aktywuj nasłuchiwanie na sygnały SIGTERM i SIGINT
11 app.enableShutdownHooks();
12
13 // SIGTERM - wysyłany przez Docker/Kubernetes przy zatrzymywaniu
14 // SIGINT - wysyłany przez Ctrl+C w terminalu
15
16 const port = process.env.PORT || 3000;
17 await app.listen(port);
18 logger.log(`Imperium nasłuchuje na porcie ${port}`);
19}
20
21bootstrap();Jednym z najważniejszych aspektów jest bezpieczne rozłączenie z bazą danych:
1// database-shutdown.service.ts
2import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
3import { InjectConnection } from '@nestjs/mongoose';
4import { Connection } from 'mongoose';
5
6@Injectable()
7export class DatabaseShutdownService implements OnModuleDestroy {
8 private readonly logger = new Logger(DatabaseShutdownService.name);
9
10 constructor(
11 @InjectConnection() private readonly connection: Connection,
12 ) {}
13
14 async onModuleDestroy() {
15 this.logger.warn('Zamykam połączenie z bazą danych Imperium...');
16
17 try {
18 // Mongoose - zamknij połączenie gracefully
19 await this.connection.close();
20 this.logger.log('Archiwum Imperium zabezpieczone - połączenie zamknięte.');
21 } catch (error) {
22 this.logger.error('Błąd przy zamykaniu bazy danych:', error.message);
23 }
24 }
25}Niezamknięte timery mogą blokować zamknięcie procesu Node.js:
1// scheduler-cleanup.service.ts
2import { Injectable, OnModuleDestroy, Logger } from '@nestjs/common';
3
4@Injectable()
5export class SchedulerCleanupService implements OnModuleDestroy {
6 private readonly logger = new Logger(SchedulerCleanupService.name);
7 private intervals: NodeJS.Timeout[] = [];
8 private timeouts: NodeJS.Timeout[] = [];
9
10 // Rejestruj interwały przy tworzeniu
11 registerInterval(callback: () => void, ms: number): NodeJS.Timeout {
12 const interval = setInterval(callback, ms);
13 this.intervals.push(interval);
14 return interval;
15 }
16
17 registerTimeout(callback: () => void, ms: number): NodeJS.Timeout {
18 const timeout = setTimeout(callback, ms);
19 this.timeouts.push(timeout);
20 return timeout;
21 }
22
23 async onModuleDestroy() {
24 this.logger.warn('Wycofuję zwiadowców - czyszczę timery i interwały...');
25
26 this.intervals.forEach(interval => clearInterval(interval));
27 this.logger.log(`Wyczyszczono ${this.intervals.length} interwałów`);
28
29 this.timeouts.forEach(timeout => clearTimeout(timeout));
30 this.logger.log(`Wyczyszczono ${this.timeouts.length} timeoutów`);
31
32 this.intervals = [];
33 this.timeouts = [];
34 }
35}Gdy aplikacja przeżywa problemy, potrzebujemy mechanizmu recovery - automatycznego powrotu do zdrowia:
1// health-recovery.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4@Injectable()
5export class HealthRecoveryService {
6 private readonly logger = new Logger(HealthRecoveryService.name);
7 private isHealthy = true;
8 private failureCount = 0;
9 private readonly MAX_FAILURES = 3;
10
11 reportFailure(component: string) {
12 this.failureCount++;
13 this.logger.warn(
14 `Awaria komponentu ${component}! Licznik: ${this.failureCount}/${this.MAX_FAILURES}`
15 );
16
17 if (this.failureCount >= this.MAX_FAILURES) {
18 this.isHealthy = false;
19 this.logger.error('Imperium w stanie krytycznym! Uruchamiam procedurę recovery...');
20 this.startRecovery();
21 }
22 }
23
24 private async startRecovery() {
25 this.logger.warn('Procedura recovery: próba przywrócenia usług...');
26
27 try {
28 await this.reconnectDatabase();
29 await this.clearCache();
30 this.failureCount = 0;
31 this.isHealthy = true;
32 this.logger.log('Recovery zakończony sukcesem!');
33 } catch (error) {
34 this.logger.error('Recovery nie powiódł się:', error.message);
35 }
36 }
37
38 private async reconnectDatabase() {
39 this.logger.log('Ponowne łączenie z archiwum...');
40 await new Promise(resolve => setTimeout(resolve, 1000));
41 }
42
43 private async clearCache() {
44 this.logger.log('Czyszczenie skrytek cache...');
45 await new Promise(resolve => setTimeout(resolve, 500));
46 }
47
48 getHealthStatus() {
49 return {
50 healthy: this.isHealthy,
51 failureCount: this.failureCount,
52 maxFailures: this.MAX_FAILURES,
53 };
54 }
55}Circuit Breaker to wzorzec chroniący aplikację przed kaskadowymi awariami - jak zamykanie bram miasta gdy wróg atakuje:
1// circuit-breaker.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3
4enum CircuitState {
5 CLOSED = 'CLOSED', // Wszystko działa - bramy otwarte
6 OPEN = 'OPEN', // Awaria - bramy zamknięte
7 HALF_OPEN = 'HALF_OPEN' // Testujemy - bramy uchylone
8}
9
10@Injectable()
11export class CircuitBreakerService {
12 private readonly logger = new Logger(CircuitBreakerService.name);
13 private state = CircuitState.CLOSED;
14 private failureCount = 0;
15 private lastFailureTime = 0;
16 private readonly FAILURE_THRESHOLD = 5;
17 private readonly RECOVERY_TIMEOUT = 30000; // 30 sekund
18
19 async execute<T>(operation: () => Promise<T>): Promise<T> {
20 if (this.state === CircuitState.OPEN) {
21 if (Date.now() - this.lastFailureTime > this.RECOVERY_TIMEOUT) {
22 this.state = CircuitState.HALF_OPEN;
23 this.logger.warn('Circuit HALF_OPEN - testujemy jednego posłańca...');
24 } else {
25 throw new Error('Circuit OPEN - bramy Imperium zamknięte!');
26 }
27 }
28
29 try {
30 const result = await operation();
31
32 if (this.state === CircuitState.HALF_OPEN) {
33 this.state = CircuitState.CLOSED;
34 this.failureCount = 0;
35 this.logger.log('Circuit CLOSED - bramy ponownie otwarte!');
36 }
37
38 return result;
39 } catch (error) {
40 this.failureCount++;
41 this.lastFailureTime = Date.now();
42
43 if (this.failureCount >= this.FAILURE_THRESHOLD) {
44 this.state = CircuitState.OPEN;
45 this.logger.error(
46 `Circuit OPEN po ${this.failureCount} awariach - zamykam bramy!`
47 );
48 }
49
50 throw error;
51 }
52 }
53
54 getState() {
55 return {
56 state: this.state,
57 failureCount: this.failureCount,
58 threshold: this.FAILURE_THRESHOLD,
59 };
60 }
61}Warto wiedzieć kiedy i dlaczego otrzymujemy te sygnały:
1// main.ts - zaawansowana obsługa sygnałów
2process.on('SIGTERM', () => {
3 console.log('Otrzymano SIGTERM - rozpoczynam graceful shutdown...');
4});
5
6process.on('SIGINT', () => {
7 console.log('Otrzymano SIGINT - natychmiastowe zamknięcie...');
8});
9
10// Złapanie nieobsłużonych wyjątków
11process.on('uncaughtException', (error) => {
12 console.error('Nieobsłużony wyjątek:', error);
13 process.exit(1);
14});
15
16process.on('unhandledRejection', (reason) => {
17 console.error('Nieobsłużona obietnica:', reason);
18});Graceful shutdown i recovery to fundamenty niezawodnego Imperium. Jak mówił Seneka: "Szczęście sprzyja przygotowanym" - aplikacja, która potrafi się bezpiecznie zamknąć i odzyskać po awarii, jest jak legion gotowy na każdą ewentualność.