Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Graceful Shutdown i Recovery - godny odwrót legionów

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ą.

Czym jest Graceful Shutdown?

Gdy aplikacja NestJS otrzymuje sygnał zamknięcia (np. SIGTERM podczas deploymentu), musi:

  • Dokończyć aktywne żądania - nie porzucaj legionistów w trakcie walki
  • Zamknąć połączenia z bazą danych - zabezpiecz archiwa Imperium
  • Wyczyścić timery i interwały - wycofaj zwiadowców
  • Rozłączyć klientów WebSocket - odwołaj posłańców
  • Zapisać stan aplikacji - sporządź raport z pola bitwy

Lifecycle Hooks w NestJS

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}

Aktywacja Shutdown Hooks

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();

Bezpieczne zamykanie połączeń z bazą danych

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}

Czyszczenie timerów i interwałów

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}

Health Check Recovery Pattern

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 Pattern - zabezpieczenie murów Imperium

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}

Obsługa sygnałów SIGTERM i SIGINT

Warto wiedzieć kiedy i dlaczego otrzymujemy te sygnały:

  • SIGTERM - Docker/Kubernetes wysyła przy zatrzymywaniu kontenera (masz ~30s na zamknięcie)
  • SIGINT - Ctrl+C w terminalu (natychmiastowe zamknięcie)
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ść.

Vai a CodeWorlds