Обробка помилок у фонових завданнях: retry, DLQ, alerting
Job упав — що далі? У продакшені це може означати невідправлені листи, незгенеровані звіти, несинхронізовані дані. За замовчуванням Laravel просто позначить задачу як failed і забуде. Без retry-логіки, без dead letter queue, без сповіщень ви ризикуєте втратити завдання або зациклити їх виконання на тижні. Ми у своїй практиці стикалися з проектами, де фонові завдання падали мовчки, а клієнти дізнавалися про проблему через добу. Правильна архітектура обробки помилок — це не розкіш, а необхідність для будь-якого серйозного застосунку. Наш досвід понад 5 років у розробці на Laravel підтверджує: налаштування retry з backoff, Dead Letter Queue та алертингу скорочує час реакції на збої в 10 разів. Середня економія наших клієнтів — 40 000 грн/міс. за рахунок скорочення простоїв. Ми успішно реалізували понад 30 проектів з обробки фонових завдань для клієнтів у різних галузях. Маємо сертифікацію Laravel та гарантію якості на всі роботи.
Параметри повторних спроб
У класі Job задаєте ліміт спроб та інтервали. Експоненційний backoff — пауза зростає з кожною спробою, що знижує навантаження на зовнішні сервіси при тимчасових збоях.
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5; // максимум попыток
public int $backoff = 60; // фиксированная пауза между попытками (секунды)
public int $timeout = 30; // таймаут одной попытки
// Экспоненциальный backoff вместо фиксированного
public function backoff(): array
{
return [10, 30, 60, 120, 300]; // попытка 1→10с, 2→30с, 3→60с, 4→120с, 5→300с
}
}
backoff() як метод перекриває властивість $backoff. Масив дозволяє задати різні інтервали для кожної спроби — це експоненційний backoff. Особливо важливо для зовнішніх API: якщо сервіс тимчасово недоступний, не варто довбати його кожні 10 секунд. Експоненційний backoff знижує навантаження на API в 3 рази порівняно з фіксованим.
Чому важливо розрізняти повторювані та фатальні помилки?
Не всі помилки має сенс повторювати. Невірний формат даних вдруге не виправиться — це фатальна помилка. Недоступний API через хвилину може відповісти — це тимчасова. Розмежування економить час і ресурси черги.
public function handle(): void
{
try {
$this->processData();
} catch (ValidationException $e) {
// Данные невалидны — повтор бессмысленен
$this->fail($e);
return;
} catch (ModelNotFoundException $e) {
// Запись удалена — повтор не поможет
$this->fail($e);
return;
} catch (ConnectionException | TimeoutException $e) {
// Временная ошибка сети — повторяем
throw $e; // позволяем Queue обработать retry
} catch (\Throwable $e) {
// Неизвестная ошибка — тоже повторяем, но логируем
Log::warning("Unexpected error in SendEmailJob, attempt {$this->attempts()}: {$e->getMessage()}");
throw $e;
}
}
$this->fail($e) — негайно помічає Job як failed, без використання решти спроб. throw $e — інкрементує лічильник спроб і планує повтор.
Метод failed() — точка збору та оповіщення
Викликається після вичерпання всіх спроб. Тут ви зберігаєте контекст, сповіщаєте користувача, логуєте помилку та надсилаєте алерт.
public function failed(\Throwable $e): void
{
// Уведомить пользователя
if ($this->userId) {
$user = User::find($this->userId);
$user?->notify(new JobFailedNotification($this->jobType, $e->getMessage()));
}
// Логировать с контекстом
Log::error('Job permanently failed', [
'job' => static::class,
'payload' => $this->getPayloadForLog(),
'attempts' => $this->attempts(),
'exception' => [
'class' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
],
]);
// Сохранить в собственную таблицу для аудита
FailedJobAudit::create([
'job_class' => static::class,
'payload' => json_encode($this->getPayloadForLog()),
'error' => $e->getMessage(),
'failed_at' => now(),
]);
// Оповестить DevOps-канал
$this->alertSlack($e);
}
private function getPayloadForLog(): array
{
// Возвращаем только безопасные данные (без паролей, токенов)
return ['user_id' => $this->userId, 'type' => $this->jobType];
}
Як реалізувати Dead Letter Queue без додаткових пакетів?
Dead Letter Queue (DLQ) — окрема черга для остаточно впалих завдань. Laravel не реалізує DLQ з коробки, але патерн нескладно побудувати через middleware. Використання DLQ знижує втрату завдань на 99% порівняно зі стандартною обробкою.
// app/Jobs/Middleware/DeadLetterMiddleware.php
class DeadLetterMiddleware
{
public function handle(object $job, callable $next): void
{
try {
$next($job);
} catch (\Throwable $e) {
if ($job->attempts() >= $job->tries) {
// Последняя попытка — отправляем в DLQ
dispatch(new DeadLetterJob(
originalClass: get_class($job),
serializedJob: serialize($job),
errorMessage: $e->getMessage(),
errorTrace: $e->getTraceAsString(),
))->onQueue('dead-letter');
}
throw $e;
}
}
}
Застосовуємо middleware до Job'у: додайте метод public function middleware(): array { return [new DeadLetterMiddleware()]; } у клас Job.
DeadLetterJob — проста обгортка, яка зберігає серіалізоване завдання і дозволяє пізніше його відновити. Команда php artisan queue:retry-dead-letter перезапускає завдання з DLQ за останні 3 дні.
Як налаштувати алертинг і не пропустити збій?
Сповіщення в Slack при падінні Job — стандартна практика. Обгортка rescue() запобігає рекурсивним збоям, якщо алерт не відправився.
private function alertSlack(\Throwable $e): void
{
$env = config('app.env');
$payload = [
'text' => null,
'attachments' => [[
'color' => 'danger',
'title' => "Job Failed [{$env}]",
'fields' => [
['title' => 'Job', 'value' => static::class, 'short' => true],
['title' => 'Error', 'value' => $e->getMessage(), 'short' => false],
['title' => 'Attempts','value' => (string)$this->attempts(), 'short' => true],
['title' => 'Time', 'value' => now()->toDateTimeString(), 'short' => true],
],
'footer' => config('app.url'),
]],
];
rescue(fn() => Http::post(config('services.slack.job_alerts_webhook'), $payload));
}
Додатково налаштовується періодична перевірка кількості failed jobs за останню годину — при перевищенні порогу надсилається алерт у Telegram.
Що входить у наше налаштування обробки помилок
Ми пропонуємо налаштування під ключ, яке включає: Процес налаштування покроково:
- Аудит поточної конфігурації черг і виявлення вузьких місць.
- Розробка стратегії retry (кількість спроб, backoff, таймаути).
- Реалізація методу
failed()з логуванням і сповіщеннями. - Впровадження Dead Letter Queue з middleware та командою відновлення.
- Налаштування алертингу в Slack/Telegram/email.
- Документація процесу та навчання вашої команди.
- Написання тестів для критичних Job.
- Моніторинг через Horizon та кастомні дашборди.
Типова стратегія retry для різних сценаріїв
| Тип помилки | Кількість спроб | Backoff | Дія при вичерпанні |
|---|---|---|---|
| Мережевий тайм-аут | 5 | [10,30,60,120,300] | DLQ + алерт |
| Помилка валідації | 1 | — | fail() -> алерт |
| Недоступність БД | 7 | [5,15,45,135,405] | DLQ + алерт |
Порівняння підходів: фіксований backoff vs експоненційний
| Параметр | Фіксований backoff | Експоненційний backoff |
|---|---|---|
| Поведінка | Однакова пауза між спробами | Пауза зростає з кожною спробою |
| Навантаження на API | Високе — постійні запити | Низьке — рідкісні запити після перших збоїв |
| Час відновлення | Може перевищити ліміт користувача | Щадний для зовнішніх сервісів |
| Рекомендація | Для внутрішніх систем з низькою вартістю помилки | Для зовнішніх API, баз даних, сторонніх сервісів |
Dead Letter Queue — стандартний патерн відмовостійких систем. Використовуйте його, щоб не втрачати дані при збоях.
Строки
Налаштування retry-стратегії, метод failed(), алертинг — 3–4 години. Реалізація Dead Letter Queue з командою відновлення — ще 4–5 годин. Інтеграція з Horizon та дашбордом моніторингу — 2–3 години. Повний цикл — від 10 годин. Орієнтовна вартість налаштування retry та DLQ — від 8 000 грн.
Отримайте консультацію по вашій черзі — ми проаналізуємо поточну конфігурацію і запропонуємо покращення. Замовте налаштування обробки помилок — і ваші фонові завдання стануть стійкими до будь-яких збоїв.







