Laravel и закон Мерфи. Очереди, задачи и ошибки.

Очередь задач в Laravel

«Всё, что может пойти не так, пойдет не так», — утверждает хорошо известный закон Мерфи. Множество вещей можно отнести к этой популярной поговорке, но особенно нужно его учитывать при разработке ПО: надейтесь на лучшее, но будьте готовым к худшему.

Закон Мерфи и разработка ПО

При разработке программного обеспечения всегда полезно задуматься о том, что делать, если что-то пойдет не так. Особенно при работе с внешними API. Ошибка может даже возникнуть не из-за вашего кода, но — как мы узнали из закона Мерфи — мы должны готовиться к худшему. Внешний API может привести к тайм-ауту, выдать ошибку 503 (превышен лимит скорости), получить критическое изменение в коде… Много чего может пойти не так.

Давайте посмотрим, как можно решить эти проблемы в Laravel.

Инкапсуляция Очереди

При работе с внешним API может оказаться целесообразным разделить вызовы к нему по отдельным процессам. У Laravel есть отличный способ инкапсуляции: Queued Jobs (Задачи в очереди). Очереди имеют несколько преимуществ:

  • Процессы асинхронны (происходят в фоновом режиме).
  • Они могут автоматически пытаться выполнить задачи несколько раз
  • Задачи можно повторить вручную
  • Понимание, почему эта конкретная задача не удалась

Основы использования Queued Jobs в Laravel

Задача в Laravel ничем не отличается от простого PHP-класса с методом handle(). Мы указываем, что он должен быть поставлен в очередь, добавляя интерфейс ShouldQueue. Что бы ни происходило в методе handle, оно будет выполнено, когда задача извлечется из очереди.

class CallExternalApi implements ShouldQueue
{
    public function handle(ExternalApi $externalApi)
    {
        $externalApi->call();
    }
}

Мы можем отправить задачу в очередь, используя хелпер dispatch:

dispatch(new CallExternalApi);

Воркер очереди можно запустить при помощи команды artisan queue:work.

php artisan queue:work

В командной строке будет показано, что задача обработана:

[2019-06-26 14:03:26][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:26][12] Processed: App\Jobs\CallExternalApi

Когда что-то идет не так

Как гласит закон Мерфи: всё пойдет не так. Когда задача не выполнилась, то мы видим, что по дефолту Laravel продолжает попытки её выполнить без каких-либо задержек.

[2019-06-26 14:03:26][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:27][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:28][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:29][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:20][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:31][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:32][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:33][12] Processing: App\Jobs\CallExternalApi
...

Ограничение попыток

Одним из решений этой проблемы является ограничение количества повторных попыток. Самый быстрый способ —  указать это в команде artisan.

php artisan queue:work --tries=3

Теперь, когда очередь попыталась выполнить задачу 3 раза, то она отметит его как проваленное.

[2019-06-26 14:03:26][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:27][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:28][12] Processing: App\Jobs\CallExternalApi
[2019-06-26 14:03:29][12] Failed: App\Jobs\CallExternalApi

Уведомление пользователя о сбое

У Laravel есть способ подключения к проваленным задачам при помощи метода failed() в классе задач. Вы можете использовать этот хук, чтобы уведомить пользователя о сбое, отправить текстовое сообщение о том, что что-то идет не так, и т.д. Хук отработает только после последней попытки.

class CallExternalApi implements ShouldQueue
{
    public function __construct(User $user)
    {
         $this->user = $user;
    }    
    
    public function handle(ExternalApi $externalApi)
    {
        $externalApi->call();
    }

    public function failed()
    {
        $this->user->notify(new ExternalApiCallFailedNotification);
    }
}

Повторная попытка выполнения задачи

Laravel может сохранить все проваленные задачи в базе данных. Но, сначала вы должны создать таблицу failed_jobs, выполнив:

php artisan queue:failed-table
php artisan migrate

Таблица failed_jobs содержит информацию о соединении, очереди, полезной нагрузке и сгенерированном исключении.

Мы можем просмотреть все невыполненные задачи, запустив queue:failed:

php artisan queue:failed

Эта команда покажет вам следующее:

Таблица из БД показывает дополнительную информацию, такую как полезная нагрузка и выданные исключения. Это также может быть отображено с помощью Laravel Horizon.

Сохраняя эту информацию, Laravel позволяет нам повторить задачу позже. Это может быть удобно, когда в коде была ошибка. После исправления её мы можем повторить задание, выполнив команду queue:retry.

php artisan queue:retry 1

Задача (с идентификатором 1) будет возвращена обратно в очередь и выполнена воркером.

Автоматизация процесса повтора

Возможно, не всегда необходимо повторять попытки вручную. Внешний API может выдавать тайм-аут или ошибку 503 — в этих ситуациях мы хотим автоматически повторить попытку несколько раз, но с задержкой между попытками.

Отсрочка попыток

Laravel позволяет нам определить глобальную задержку для всех задач, выполняемых одним и тем же воркером, указав параметр —delay:

php artisan queue:work --tries=3 --delay=3

Теперь воркер будет ждать 3 секунды, прежде чем попробует еще раз выполнить проваленную задачу.

Ситуация со специфическими повторами и задержками

Используя параметры командной строки, мы указываем попытки и задержку для всех задач. В некоторых случаях этого может быть недостаточно. Некоторым задачам может быть разрешено иметь больше попыток или нужно иметь большую задержку. Laravel позволяет нам указать эти настройки в классе задач:

class CallExternalApi implements ShouldQueue
{
     use InteractsWithQueue;   
  
     /**
     * The number of times the job may be attempted.
     *
     * @var int
     */
     public $tries = 5; 
  
     /**
     * The number of seconds to wait before retrying the job.
     *
     * @var int
     */
     public $retryAfter = 5;
}

Если эти параметры указаны в задачи, то они будет иметь приоритет над значениями, указанным в командной строке.

Экспоненциальная стратегия отсрочки

В ситуациях, подобных ошибке 503 у внешнего API, может потребоваться увеличить задержку после каждой попытки. Laravel может и это, просто нужно указать метод retryAfter() в классе задач. Через трейт InteractsWithQueue мы можем получить количество попыток.

class CallExternalApi implements ShouldQueue
{
    use InteractsWithQueue;    
    
    /**
     * The number of times the job may be attempted.
     *
     * @var int
     */
    public $tries = 10;    
    
    /**     
     * @return Carbon     
     */    
     public function retryAfter()    
     {   
         return now()->addSeconds(            
             $this->attempts() * 2      
         );    
     }
}

Приведенный выше пример увеличит задержку при помощи линейной кривой. Если мы хотим реализовать экспоненциальный подход, то можем использовать формулу экспоненциальной отсрочки:

Формула экспоненциальной отсрочки

В Laravel это реализуется следующим образом:

public function retryAfter()    
{   
    return now()->addSeconds(            
        (int) round(((2 ** $this->attempts()) - 1 ) / 2)        
    );    
}

Теперь время повтора будет расти экспоненциально, пока не будет достигнуто максимальное количество попыток, как показано на этой диаграмме:

Очереди задач в Ларавел

Примечание: если вы хотите, чтобы было много попыток с использованием экспоненциальной отсрочки, то быстро возникнут часовые задержки. В таких случаях, возможно, вы захотите использовать логарифмическую функцию.

Заключение

При разработке программного обеспечения думайте не только о позитивных путях. Запишите (желательно с (модульными) тестами), всё что может пойти не так. Затем разрабатывайте свою систему так, чтобы иметь возможность покрыть эти ситуации. (Независимо от того, автоматически или нет). Не существует единого решения для управления всеми этими процессами, некоторые процессы могут нуждаться в специфической обработке ошибок, в то время как с другими может сработать дефолтный подход.

Автор: Patrick Brouwers
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.