Пошук уроків, статей та іншого контенту
Реалізуєте graceful shutdown, щоб завершувати запити й закривати ресурси без втрати даних.
Коли процес NestJS отримує сигнал завершення, наприклад SIGTERM від Docker або Kubernetes, його не варто зупиняти миттєво. Різке завершення може призвести до того, що:
поточний HTTP-запит не встигне завершитися;
запис у базу даних або чергу залишиться незавершеним;
відкриті з'єднання не буде коректно закрито;
буферизовані логи або повідомлення можуть бути втрачені.
Graceful shutdown — це послідовне завершення роботи застосунку:
процес отримує сигнал завершення;
NestJS перестає приймати нові підключення;
поточні запити отримують час на завершення;
викликаються lifecycle hooks модулів;
закриваються бази даних, черги, таймери та інші ресурси;
процес завершується.
За замовчуванням NestJS не підписується на системні сигнали завершення. Для цього в main.ts потрібно викликати enableShutdownHooks().
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Підписуємося на сигнали коректного завершення
app.enableShutdownHooks(['SIGINT', 'SIGTERM']);
await app.listen(3000);
}
bootstrap();Найчастіше використовуються:
SIGTERM — стандартний сигнал завершення для контейнерів і оркестраторів;
SIGINT — сигнал переривання, наприклад після натискання Ctrl+C.
Після отримання сигналу NestJS запускає процедуру завершення застосунку та викликає відповідні lifecycle hooks.
Не потрібно вручну викликати
process.exit()одразу після отримання сигналу. Це може перервати активні запити та не дати ресурсам завершити очищення.
NestJS надає кілька hooks, які можна використати під час завершення роботи.
OnModuleDestroyВикликається, коли NestJS починає знищувати модулі. Його можна використовувати для підготовки ресурсу до завершення.
BeforeApplicationShutdownВикликається перед повним завершенням застосунку. Метод може отримати назву системного сигналу.
OnApplicationShutdownВикликається після завершення основної процедури закриття застосунку. Це зручне місце для остаточного закриття зовнішніх ресурсів.
Усі асинхронні операції очищення потрібно повертати з hook як Promise, щоб NestJS міг їх дочекатися.
Нижче наведено приклад застосунку, який:
має повільний HTTP-запит;
запускає фоновий таймер;
коректно очищає таймер під час завершення;
реагує на SIGINT і SIGTERM.
// app.module.ts
import {
Controller,
Get,
Injectable,
Logger,
Module,
OnApplicationShutdown,
OnModuleInit,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
@Injectable()
class BackgroundWorker implements OnModuleInit, OnApplicationShutdown {
private readonly logger = new Logger(BackgroundWorker.name);
private timer?: NodeJS.Timeout;
private isClosed = false;
onModuleInit(): void {
this.logger.log('Фоновий ресурс запущено');
this.timer = setInterval(() => {
this.logger.log('Фоновий ресурс виконує роботу');
}, 5000);
}
async onApplicationShutdown(signal?: string): Promise<void> {
if (this.isClosed) {
return;
}
this.isClosed = true;
this.logger.log(
`Починаємо очищення ресурсу. Сигнал: ${signal ?? 'app.close()'}`,
);
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
// Імітуємо очікування завершення операції ресурсу
await sleep(500);
this.logger.log('Фоновий ресурс коректно закрито');
}
}
@Controller()
class AppController {
@Get('slow')
async slowRequest(): Promise<{ status: string }> {
await sleep(3000);
return {
status: 'запит завершено',
};
}
}
@Module({
controllers: [AppController],
providers: [BackgroundWorker],
})
class AppModule {}
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// Дозволяємо NestJS обробляти сигнали завершення
app.enableShutdownHooks(['SIGINT', 'SIGTERM']);
await app.listen(3000);
}
bootstrap();Якщо під час виконання GET /slow процес отримує SIGTERM, NestJS починає завершення застосунку. Поточний запит отримує можливість завершитися, після чого NestJS викликає onApplicationShutdown() і закриває фоновий ресурс.
У реальному застосунку замість таймера тут може бути:
підключення до бази даних;
клієнт брокера повідомлень;
пул з'єднань;
фоновий worker;
клієнт зовнішнього сервісу.
Кожен ресурс, який створює довготривале підключення, повинен мати симетричне очищення.
Спрощений приклад сервісу з підключенням:
import {
Injectable,
Logger,
OnModuleInit,
OnApplicationShutdown,
} from '@nestjs/common';
@Injectable()
export class DatabaseService
implements OnModuleInit, OnApplicationShutdown
{
private readonly logger = new Logger(DatabaseService.name);
private connected = false;
async onModuleInit(): Promise<void> {
// Тут може бути реальне підключення до бази даних
this.connected = true;
this.logger.log('Підключення до бази даних встановлено');
}
async onApplicationShutdown(signal?: string): Promise<void> {
if (!this.connected) {
return;
}
this.logger.log(
`Закриваємо базу даних. Сигнал: ${signal ?? 'app.close()'}`,
);
// Тут має бути виклик close або disconnect конкретного клієнта
this.connected = false;
this.logger.log('Підключення до бази даних закрито');
}
}Важливі властивості такого hook:
він безпечний для повторного виклику;
він очікує завершення асинхронної операції;
він не приховує помилки закриття;
він не викликає process.exit().
Якщо бібліотека для роботи з базою даних має власний метод close(), disconnect() або подібний, його потрібно викликати саме в shutdown hook.
Під час завершення NestJS lifecycle hooks викликаються в межах загального процесу завершення застосунку. Практично це означає:
застосунок отримує системний сигнал;
NestJS починає закривати HTTP-сервер;
виконується код lifecycle hooks;
NestJS очікує завершення асинхронних операцій;
закриваються підключення та інші ресурси;
процес завершується.
Точна поведінка також залежить від адаптера та зовнішньої бібліотеки. Наприклад, HTTP-сервер може чекати завершення вже розпочатих запитів, але нові підключення після початку завершення прийматися не повинні.
Ресурси, потрібні обробникам запитів, не слід закривати до того, як ці запити завершаться. Наприклад, передчасне закриття бази даних може спричинити помилки в обробниках, які ще виконуються.
Shutdown-код має бути ідемпотентним: повторний виклик не повинен призводити до помилки.
Це особливо важливо, якщо:
ресурс уже було закрито раніше;
застосунок закривається з тесту через app.close();
кілька частин коду можуть ініціювати завершення;
hook отримує помилку під час попередньої спроби очищення.
Приклад перевірки стану:
private closed = false;
async onApplicationShutdown(): Promise<void> {
if (this.closed) {
return;
}
this.closed = true;
await this.client.close();
}Також варто перевіряти наявність ресурсу перед очищенням:
if (this.connection) {
await this.connection.close();
this.connection = undefined;
}Graceful shutdown не повинен тривати нескінченно. Наприклад, зовнішній сервіс може не відповідати під час закриття з'єднання.
Обмеження часу зазвичай налаштовують на рівні середовища запуску або оркестратора. Якщо застосунок має власний максимальний час завершення, після його спливання може знадобитися примусове завершення процесу. Такий сценарій потрібно розглядати як аварійний fallback, а не як основний шлях.
Під час налаштування тайм-ауту потрібно врахувати:
максимальну тривалість звичайного запиту;
час завершення фонових операцій;
час закриття з'єднань;
час, який середовище запуску надає процесу після SIGTERM.
Занадто короткий тайм-аут зводить graceful shutdown нанівець, оскільки активні операції будуть перервані.
Перевірити механізм можна локально:
запустіть застосунок;
відкрийте GET /slow;
поки запит виконується, натисніть Ctrl+C;
перевірте, що запит завершився;
перевірте логи закриття ресурсів;
переконайтеся, що процес завершився без зависання.
Для перевірки сигналу SIGTERM можна надіслати його процесу з іншого термінала. У середовищах на кшталт Docker саме цей сигнал зазвичай використовується для коректної зупинки контейнера.
Під час інтеграційних тестів можна викликати:
await app.close();Це також запускає процедуру закриття та допомагає перевірити, чи всі ресурси очищаються після тестів.
enableShutdownHooks()Без цього виклику NestJS може не обробляти системні сигнали та не запускати shutdown hooks.
process.exit() одразу після сигналуПримусове завершення може перервати:
HTTP-запити;
транзакції;
запис логів;
очищення ресурсів.
Якщо примусове завершення потрібне через зависання, воно має бути лише резервним сценарієм після завершення дозволеного тайм-ауту.
Помилка:
onApplicationShutdown(): void {
this.client.close();
}У цьому випадку NestJS може не дочекатися завершення close().
Правильніше:
async onApplicationShutdown(): Promise<void> {
await this.client.close();
}Завершення HTTP-сервера не закриває автоматично всі ресурси, створені застосунком. Потрібно окремо очищати бази даних, черги, таймери та інші довготривалі ресурси.
Повторний виклик методу close() деяких клієнтів може спричинити помилку. Зберігайте стан ресурсу або використовуйте перевірку його наявності.
Фоновий worker повинен мати механізм зупинки. Таймери, цикли та споживачі повідомлень, які не реагують на завершення, можуть утримувати процес або затримувати його зупинку.
Для обробки системних сигналів увімкніть app.enableShutdownHooks().
Закривайте ресурси в OnApplicationShutdown, BeforeApplicationShutdown або OnModuleDestroy.
Асинхронні операції очищення повертайте через Promise.
Не викликайте process.exit() одразу після отримання сигналу.
Робіть shutdown-код ідемпотентним.
Враховуйте час завершення активних HTTP-запитів і фонових операцій.
Перевіряйте graceful shutdown як через системні сигнали, так і через app.close() у тестах.