Пошук уроків, статей та іншого контенту
Навчитеся взаємодіяти із системним середовищем через сигнали, коди завершення та контроль життєвого циклу процесу.
Node.js запускається всередині операційної системи, тому процес має доступ до:
змінних середовища;
аргументів командного рядка;
сигналів операційної системи;
стандартних потоків вводу та виводу;
коду завершення;
подій життєвого циклу процесу.
Основний об’єкт для взаємодії з цим середовищем — глобальний об’єкт process.
Змінні середовища доступні через process.env:
const port = process.env.PORT ?? '3000';
const environment = process.env.NODE_ENV ?? 'development';
console.log(`Порт: ${port}`);
console.log(`Середовище: ${environment}`);Усі значення в process.env є рядками або undefined, якщо змінну не задано.
Запустити програму зі змінними середовища можна так:
PORT=8080 NODE_ENV=production node app.jsУ PowerShell синтаксис відрізняється:
$env:PORT = "8080"
$env:NODE_ENV = "production"
node app.jsЗміна властивості process.env змінює середовище поточного процесу та процесів, які він запускатиме надалі. Вона не змінює середовище батьківського процесу:
process.env.APP_MODE = 'test';
console.log(process.env.APP_MODE); // testНе варто безпосередньо покладатися на значення змінних середовища без перевірки. Наприклад, порт потрібно перетворити на число й перевірити його діапазон.
Сигнал — це повідомлення від операційної системи або іншого процесу до процесу Node.js.
Поширені сигнали:
SIGINT — переривання з термінала, зазвичай надходить після натискання Ctrl+C;
SIGTERM — прохання завершити процес;
SIGHUP — завершення або розрив термінального сеансу;
SIGQUIT — завершення з можливістю створення дампа;
SIGKILL — безумовне завершення процесу;
SIGSTOP — призупинення процесу.
Обробник сигналу встановлюється через process.on:
process.on('SIGINT', () => {
console.log('Отримано SIGINT');
});Після встановлення обробника Node.js більше не виконує стандартну поведінку для цього сигналу автоматично. Тому обробник має сам завершити процес або запустити контрольоване завершення.
Процес може надіслати сигнал іншому процесу через process.kill:
process.kill(process.pid, 'SIGTERM');Перший аргумент — ідентифікатор процесу, другий — назва сигналу.
З термінала процесу можна надіслати сигнал за його PID:
kill -TERM 12345Для SIGINT зазвичай достатньо натиснути Ctrl+C.
SIGKILL не можна перехопити, ігнорувати або обробити. Якщо процес має коректно закрити з’єднання, зберегти дані чи звільнити ресурси, для цього потрібно використовувати SIGTERM або SIGINT.
Підтримка сигналів залежить від операційної системи. Найповніша модель сигналів доступна в Unix-подібних системах. У Windows частина сигналів має іншу поведінку або підтримується лише частково.
Є два основні способи задати код завершення.
process.exitCodeprocess.exitCode задає код, з яким Node.js завершить процес після спорожнення циклу подій:
process.exitCode = 1;Це рекомендований спосіб завершення після асинхронної роботи. Node.js ще має час завершити відкриті операції.
process.exit()process.exit() завершує процес негайно:
process.exit(1);Негайне завершення може перервати:
запис у stdout або stderr;
мережеві операції;
закриття файлів;
асинхронне очищення ресурсів;
надсилання відповідей клієнтам.
Тому process.exit() варто залишати для аварійних ситуацій або для моменту, коли всі необхідні операції вже завершені.
Зазвичай використовують такі значення:
0 — успішне завершення;
ненульове значення — помилка або нештатне завершення.
Приклад:
const configurationIsValid = false;
if (!configurationIsValid) {
console.error('Некоректна конфігурація');
process.exitCode = 2;
}Код можна прочитати в оболонці.
У Bash:
node app.js
echo $?У PowerShell:
node app.js
$LASTEXITCODEУ Unix-подібних системах оболонка часто використовує значення 128 + номер сигналу, якщо процес завершився через сигнал. Наприклад, завершення через SIGTERM часто спостерігається як код 143. Це загальна конвенція оболонки, а не спосіб, на який слід покладатися у прикладній логіці.
Сервер не повинен просто завершуватися після отримання SIGTERM. Зазвичай він має:
перестати приймати нові з’єднання;
дозволити поточним запитам завершитися;
закрити бази даних, черги та інші ресурси;
встановити коректний код завершення;
завершитися примусово, якщо очищення зависло.
Приклад HTTP-сервера з контрольованим завершенням:
const http = require('node:http');
const rawPort = process.env.PORT ?? '3000';
const port = Number.parseInt(rawPort, 10);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
console.error(`Некоректний PORT: ${rawPort}`);
process.exitCode = 2;
} else {
const server = http.createServer((request, response) => {
response.end('OK\n');
});
const sockets = new Set();
let shuttingDown = false;
server.on('connection', (socket) => {
sockets.add(socket);
socket.on('close', () => {
sockets.delete(socket);
});
});
server.on('error', (error) => {
console.error('Помилка сервера:', error.message);
process.exitCode = 1;
});
server.listen(port, () => {
console.log(`Сервер слухає порт ${port}`);
});
function shutdown(signal) {
if (shuttingDown) {
console.error(`Повторний сигнал ${signal}; завершення примусово`);
process.exit(1);
}
shuttingDown = true;
console.log(`Отримано ${signal}. Починається завершення`);
// Перестаємо приймати нові з'єднання та чекаємо на поточні.
server.close((error) => {
clearTimeout(forceExitTimer);
if (error) {
console.error('Помилка під час закриття сервера:', error.message);
process.exitCode = 1;
} else {
console.log('Сервер коректно завершив роботу');
process.exitCode = 0;
}
});
const forceExitTimer = setTimeout(() => {
console.error('Час очікування вичерпано; активні з’єднання буде закрито');
// Закриваємо з'єднання, які не завершилися самостійно.
for (const socket of sockets) {
socket.destroy();
}
process.exitCode = 1;
process.exit();
}, 10_000);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}Запустити приклад:
PORT=8080 node server.jsВ іншому терміналі можна надіслати сигнал:
kill -TERM <PID>Або запустити сервер у передньому плані та натиснути Ctrl+C.
Сигнал може надійти повторно. Наприклад, оркестратор спочатку надсилає SIGTERM, а через певний час — інший сигнал для примусового завершення.
Функція завершення має захищатися від повторного запуску:
let shuttingDown = false;
function shutdown() {
if (shuttingDown) {
return;
}
shuttingDown = true;
// Очищення ресурсів виконується лише один раз.
}Без такого захисту можна двічі закрити сервер, повторно завершити транзакцію або виконати очищення в некоректному порядку.
beforeExitПодія beforeExit виникає, коли в циклі подій більше немає роботи, але процес ще можна продовжити:
process.on('beforeExit', (code) => {
console.log(`Процес майже завершився з кодом ${code}`);
});Якщо в обробнику додати асинхронну роботу, процес може продовжити існування:
process.on('beforeExit', () => {
setTimeout(() => {
console.log('Виконано додаткову роботу');
}, 100);
});Цю подію не слід використовувати як основний механізм завершення сервера. Вона призначена для випадку, коли цикл подій природно спорожнів.
beforeExit не викликається, якщо процес завершують через process.exit() або якщо він аварійно завершується через необроблену помилку.
exitПодія exit виникає безпосередньо перед завершенням процесу:
process.on('exit', (code) => {
console.log(`Процес завершується з кодом ${code}`);
});Обробник exit має бути синхронним. Асинхронна операція в ньому не буде гарантовано виконана:
process.on('exit', () => {
setTimeout(() => {
console.log('Цей рядок може не з’явитися');
}, 0);
});У exit можна виконати просте синхронне логування або оновити лічильник у пам’яті, але не можна покладатися на нього для закриття мережевих з’єднань чи запису файлів через асинхронні API.
Необроблена помилка зазвичай означає, що процес більше не перебуває у надійному стані. Не слід перехоплювати uncaughtException, записувати повідомлення в журнал і продовжувати роботу як ні в чому не бувало.
Якщо застосунок усе ж встановлює такий обробник для аварійного журналювання, після цього потрібно завершити процес:
process.on('uncaughtException', (error) => {
console.error('Необроблена помилка:', error);
// Процес може перебувати в непередбачуваному стані.
process.exitCode = 1;
});На практиці основне очищення краще виконувати в контрольованому shutdown, а не намагатися відновити роботу після необробленого винятку.
process.exit() одразу після помилкиconsole.error('Помилка запису');
process.exit(1);Це може обірвати вже розпочаті операції. Якщо немає потреби завершити процес негайно, краще встановити process.exitCode і дозволити циклу подій завершити роботу.
SIGTERMЗастосунок може працювати коректно локально, але некоректно завершуватися під час зупинки сервісу. Для довгоживучих процесів потрібно явно визначити політику обробки SIGTERM.
Якщо клієнт залишив з’єднання відкритим, server.close() може довго не завершуватися. Потрібен обмежений тайм-аут, після якого активні з’єднання примусово закриваються.
Кілька сигналів можуть викликати shutdown кілька разів. Прапорець стану або окремий автомат станів захищає від подвійного закриття ресурсів.
exit для асинхронного очищенняУ process.on('exit') не можна надійно виконувати асинхронний cleanup. Очищення має запускатися під час обробки сигналу, до фактичного завершення процесу.
SIGKILL можна обробитиSIGKILL не перехоплюється Node.js. Якщо застосунок не встигає завершитися після SIGTERM, операційна система може завершити його без виконання очищення. Тому важливі дані потрібно зберігати до отримання сигналу або робити операції відновлюваними.
process.env надає доступ до змінних середовища процесу.
Значення змінних середовища потрібно перевіряти та перетворювати з рядків у потрібні типи.
SIGINT зазвичай надходить після Ctrl+C, а SIGTERM використовується для контрольованої зупинки.
SIGKILL неможливо перехопити або обробити.
process.exitCode дозволяє завершити процес після спорожнення циклу подій.
process.exit() завершує процес негайно й може перервати асинхронні операції.
Graceful shutdown має зупинити приймання нових запитів, завершити поточні операції та закрити ресурси.
Обробник shutdown повинен бути ідемпотентним і мати тайм-аут для примусового завершення.
beforeExit пов’язана з природним спорожненням циклу подій, а exit призначена лише для синхронних дій перед завершенням.