Налаштування обробки помилок у Background Jobs (retry, dead letter, alerting)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Налаштування обробки помилок у Background Jobs (retry, dead letter, alerting)
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Налаштування обробки помилок у Background Jobs (retry, dead letter, alerting)

Job впав — що далі? За замовчуванням Laravel просто позначає завдання як помилкове і забуває про нього. Без логіки повторів, без dead letter queue, без сповіщень вся обробка помилок відбувається випадково. Правильна архітектура визначає: скільки разів повторити, з яким інтервалом, що робити з остаточно упалими завданнями, кого сигналізувати.

Параметри повторних спроб

У класі Job:

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 секунд.

Розрізнення помилок: повторювані vs фатальні

Не всі помилки варто повторювати. Невалідний формат даних на другий раз не виправиться — це фатальна помилка. Недоступний 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 як помилковий, без використання залишених спроб. 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 з коробки, але паттерн несложно побудувати:

// 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()];
}

DeadLetterJob — проста обгортка, яка зберігає серіалізоване завдання та дозволяє пізніше його відновити:

class DeadLetterJob implements ShouldQueue
{
    public int $tries = 1; // DLQ-завдання не повторюємо

    public function __construct(
        public string $originalClass,
        public string $serializedJob,
        public string $errorMessage,
        public string $errorTrace,
        public \Carbon\Carbon $failedAt = new \Carbon\Carbon(),
    ) {}

    public function handle(): void
    {
        // Просто зберігаємо для аудиту
        DeadLetterRecord::create([
            'original_class'  => $this->originalClass,
            'serialized_job'  => $this->serializedJob,
            'error_message'   => $this->errorMessage,
            'failed_at'       => $this->failedAt,
        ]);
    }

    public function restore(): void
    {
        $originalJob = unserialize($this->serializedJob);
        dispatch($originalJob);
    }
}

Команда для повторного запуску завдань з DLQ:

// app/Console/Commands/RetryDeadLetterJobs.php
public function handle(): void
{
    DeadLetterRecord::where('failed_at', '>=', now()->subDays(3))
        ->whereNull('retried_at')
        ->each(function (DeadLetterRecord $record) {
            $job = unserialize($record->serialized_job);
            dispatch($job);
            $record->update(['retried_at' => now()]);
            $this->info("Retried: {$record->original_class} [{$record->id}]");
        });
}

Алертинг

Сповіщення в Slack при падінні Job:

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

rescue() обгортає виклик, щоб помилка в алертингу не породжувала рекурсивного падіння.

Моніторинг кількості failed jobs

Періодична перевірка через Artisan-команду в cron:

// app/Console/Commands/CheckFailedJobs.php
public function handle(): void
{
    $count     = DB::table('failed_jobs')
        ->where('failed_at', '>=', now()->subHour())
        ->count();

    $threshold = (int) config('queue.failed_jobs_alert_threshold', 10);

    if ($count >= $threshold) {
        Http::post(config('services.telegram.webhook_url'), [
            'chat_id' => config('services.telegram.admin_chat_id'),
            'text'    => "⚠️ {$count} jobs failed in the last hour",
        ]);
    }
}
// routes/console.php
Schedule::command('queue:check-failed')->everyFiveMinutes();

Сроки

Налаштування retry-стратегії, методу failed(), алертингу — 3–4 години. Реалізація Dead Letter Queue з командою відновлення — ще 4–5 годин. Інтеграція з Horizon та дашбордом моніторингу — 2–3 години.