Коли ви розробляєте на Node.js, обробка помилок спочатку здається надто простою. Ви огортаєте маршрут у try-catch, надсилаєте статус-код 500, і клієнт сам вирішує, що робити далі. Ця модель добре працює для HTTP, але вона розвалюється, щойно ви переходите до фонових завдань. У системі черг немає клієнта, який чекає. Є лише воркер (worker), корисне навантаження (payload) і лічильник повторних спроб, що зростає десь у Redis, RabbitMQ або SQS. Якщо ви оброблятимете збої так само, як невдалі вебзапити, ви не просто втратите одну транзакцію. Ви зупините весь свій конвеєр, витратите ресурси обчислювальної потужності або будете постійно «класти» воркерів на одному й тому самому «отруєному» повідомленні.
HTTP-мислення не працює у фонових завданнях
У циклі запит-відповідь зворотний зв'язок є миттєвим. Користувач натискає кнопку, сервер видає помилку, і користувач бачить екран з повідомленням про помилку. Очищення ресурсів у такому разі просте. Воркер черги працює ізольовано. Він бере завдання, виконує його протягом секунд або хвилин, а потім підтверджує успішне завершення. Якщо посеред процесу щось іде не так, черга не знає чому. Вона знає лише те, що підтвердження так і не надійшло. Залежно від вашої конфігурації, вона намагатиметься повторити спробу, можливо, нескінченно. Одне некоректне навантаження може перескакувати між воркерами сотні разів, марнуючи ресурси CPU і ховаючись за легітимними завданнями, які дійсно потребують обробки.
Два види збоїв
Перше правило стійких черг — припинити ставитися до кожної помилки однаково. Вам потрібно розділити збої на дві групи в той самий момент, коли вони виникають.
Помилки, що підлягають повторній спробі (retryable), є тимчасовими. Це можуть бути таймаути мережі, обмеження частоти запитів (rate limits) від стороннього API або розрив з'єднання з базою даних через тимчасове вичерпання пулу з'єднань. Це симптоми живої системи під навантаженням. Вони можуть завершитися успішно під час наступної спроби через дві хвилини.
Постійні помилки — це «отруйні пігулки» (poison pills). До них належать некоректні навантаження, помилки валідації схеми або відсутність обов'язкового поля через те, що попередній сервіс змінив свій контракт. Повторювати такі спроби — марна трата ресурсів. Вони так само завершаться невдачею і на сотрій спробі.
Якщо ваш блок catch не може розрізнити ці два типи, ваша черга працює наосліп.
Патерн 1: Класифікуйте помилки у блоці catch
Блок catch вашого воркера має бути найбільш продуманою частиною коду у файлі. Коли виникає помилка, негайно проаналізуйте її. Це код помилки ECONNRESET або таймаут? Поставте її в чергу на повторну спробу. Це SyntaxError, відмова валідації Joi або відсутність обмеження зовнішнього ключа (foreign key constraint)? Одразу переміщуйте її в чергу недоставлених повідомлень (dead-letter queue, або DLQ) і не враховуйте її в ліміті повторних спроб.
Більшість бібліотек черг для Node.js, включаючи BullMQ та Bee Queue, дозволяють визначати власні стратегії затримки (backoff strategies) та хуки для помилок. Використовуйте їх. Постійна помилка ніколи не повинна просто «спати» і повторюватися тричі за замовчуванням. Її слід вивести з основної черги, щоб решта завдань могла проходити далі. DLQ зберігає саме навантаження та контекст помилки, що дозволяє вам повторно запустити завдання пізніше, після того як ви виправите баг або зміните схему.
Патерн 2: Експоненціальна затримка з джиттером (jitter)
Миттєві повторні спроби — це агресивно. Якщо база даних, що знаходиться нижче за потоком, уже задихається під навантаженням, повторні запити кожні дві секунди від п'ятдесяти воркерів остаточно її доб'ють. Вам потрібно відступити і дати системі простір для відновлення.
Використовуйте експоненціальну затримку (exponential backoff). При першій помилці зачекайте одну секунду. При другій — дві. Потім чотири, потім вісім, аж до розумного максимуму, наприклад, п'яти хвилин. Але одного таймінгу недостатньо. Якщо кожне невдале завдання використовуватиме однаковий інтервал, вони всі зіткнуться в один момент, коли час затримки вичерпається. Ця синхронізована хвиля, яку іноді називають «громом натовпу» (thundering herd), може перевантажити сервіс, що відновлюється.
Додайте джиттер (jitter). Візьміть розраховану затримку і додайте до неї випадковий відсоток, можливо, від десяти до двадцяти відсотків. Чотири секунди перетворюються на 4,2 або 4,7. Ця проста випадковість розтягує пік повторних спроб і не дозволяє вашій інфраструктурі отримувати удари хвилями.
Патерн 3: Проєктуйте з урахуванням ідемпотентності
Саме тут інфраструктура черг стикається з бізнес-логікою. Уявіть завдання, яке списує кошти з клієнта через платіжний сервіс. Воркер успішно надсилає запит на списання, але з'єднання розривається до того, як він зможе записати успіх у вашу базу даних або підтвердити виконання завдання. Черга бачить помилку. Вона повторює спробу. З клієнта списують кошти двічі.
У Node.js запобігайте цьому, роблячи кожен побічний ефект ідемпотентним. Генеруйте ключ ідемпотентності на основі ID завдання або специфічного бізнес-ідентифікатора. Перш ніж створювати платіж, надсилати електронний лист або коригувати запаси, перевірте, чи ця робота вже була виконана. Передавайте цей ключ через усю систему: у вашу базу даних та в будь-які сторонні API, які його підтримують. Структуруйте свої завдання так, щоб виконання одного й того самого payload десять разів давало такий самий результат, як і виконання один раз. Ця одна звичка усуває цілу категорію помилок, пов'язаних із фінансами та цілісністю даних.
Паттерн 4: Ставтеся до своєї Dead-Letter Queue як до дашборда
DLQ — це не цвинтар, куди потрапляють «погані» завдання, щоб про них забули. Це операційний інструмент, і він має бути однією з найбільш контрольованих поверхонь у вашій системі.
Налаштуйте сповіщення, які спрацьовують при збільшенні глибини DLQ. Навіть одне повідомлення в DLQ часто означає, що ваша логіка валідації зламана, схема upstream-сервісу змінилася або downstream-сервіс надсилає «сміття», яке ви більше не розпізнаєте. Це саме ті сигнали, які ви хочете впіймати до того, як почнуть скаржитися клієнти. Створіть дашборд, який дозволить вам переглядати raw payload, stack trace та мітку часу. Підготуйте runbook: проаналізуйте помилку, виправте код, а потім повторно відправте (replay) повідомлення у правильному порядку. Якщо ваша реалізація черги це підтримує, налаштуйте сповіщення як на швидкість зростання, так і на абсолютну кількість, оскільки один невдалий деплой може заповнити DLQ за лічені хвилини.
Паттерн 5: Якщо є сумніви — зупиняйте процес
Node.js працює на єдиному циклі подій (event loop) всередині V8 isolate. Необроблена відмова промісу (unhandled promise rejection) або
