Подписывайтесь на наш Telegram канал и будьте в курсе всех событий.
Ищете работу? Мы поможем!
Ищете работу? Мы поможем!

Планирование задач

Вы просматриваете документ для прошлой версии.
Рассмотрите возможность обновления вашего проекта до актуальной версии 11.x. Почему это важно?

Введение

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

Планировщик команд Laravel предлагает новый подход к управлению запланированными задачами на вашем сервере. Планировщик позволяет вам быстро и выразительно определять расписание команд в самом приложении Laravel. При использовании планировщика на вашем сервере требуется только одна запись cron. Расписание задач определяется в методе schedule файла app/Console/Kernel.php. Для начала работы в методе определен простой пример.

Определение расписаний

Вы можете определить все свои запланированные задачи в методе schedule класса App\Console\Kernel вашего приложения. Для начала рассмотрим пример. В этом примере мы определим замыкание, которое будет вызываться каждый день в полночь. В замыкании мы выполним запрос к базе данных для очистки таблицы:

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;

class Kernel extends ConsoleKernel
{
    /**
     * Определить расписание выполнения команд приложения.
     */
    protected function schedule(Schedule $schedule): void
    {
        $schedule->call(function () {
            DB::table('recent_users')->delete();
        })->daily();
    }
}

В дополнение к планированию с использованием замыканий вы также можете использовать вызываемые объекты. Вызываемые объекты – это простые классы PHP, содержащие метод __invoke:

$schedule->call(new DeleteRecentUsers)->daily();

Если вы хотите просмотреть список ваших запланированных задач и их последующего запуска, то вы можете использовать команду schedule:list Artisan:

php artisan schedule:list

Планирование команд Artisan

В дополнение к планированию с использованием замыканий вы также можете использовать команды Artisan и системные команды. Например, вы можете использовать метод command для планирования команды Artisan, используя имя команды или класс.

При планировании команд Artisan с использованием имени класса команды вы можете передать массив дополнительных аргументов командной строки, которые должны быть переданы команде при ее вызове:

use App\Console\Commands\SendEmailsCommand;

$schedule->command('emails:send Taylor --force')->daily();

$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

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

Метод job используется для планирования отправки задания в очередь. Этот метод обеспечивает удобный способ планирования таких заданий без использования метода call с замыканием:

use App\Jobs\Heartbeat;

$schedule->job(new Heartbeat)->everyFiveMinutes();

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

use App\Jobs\Heartbeat;

// Отправляем задание в очередь «heartbeats» соединения «sqs» ...
$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

Планирование команд операционной системы

Метод exec используется для передачи команды операционной системе:

$schedule->exec('node /home/forge/script.js')->daily();

Параметры периодичности расписания

Мы уже видели несколько примеров того, как можно настроить задачу на выполнение через определенные промежутки времени. Однако существует гораздо больше параметров планирования, которые можно назначить задаче:

Метод Описание
->cron('* * * * *'); Запустить задачу по расписанию с параметрами cron
->everySecond(); Запускать задачу ежесекундно
->everyTwoSeconds(); - каждые 2 секунды
->everyFiveSeconds(); - каждые 5 секунд
->everyTenSeconds(); - каждые 10 секунд
->everyFifteenSeconds(); - каждые 15 секунд
->everyTwentySeconds(); - каждые 20 секунд
->everyThirtySeconds(); - каждые 30 секунд
->everyMinute(); Запускать задачу ежеминутно
->everyTwoMinutes(); – каждые 2 минуты
->everyThreeMinutes(); – каждые 3 минуты
->everyFourMinutes(); – каждые 4 минуты
->everyFiveMinutes(); – каждые 5 минут
->everyTenMinutes(); – каждые 10 минут
->everyFifteenMinutes(); – каждые 15 минут
->everyThirtyMinutes(); – каждые 30 минут
->hourly(); – каждый час
->hourlyAt(17); – в 17 минут каждого часа
->everyOddHour($minutes = 0); - каждый нечетный час
->everyTwoHours($minutes = 0); - каждые 2 часа
->everyThreeHours($minutes = 0); - каждые 3 часа
->everyFourHours($minutes = 0); - каждые 4 часа
->everySixHours($minutes = 0); - каждые 6 часов
->daily(); – каждый день в полночь
->dailyAt('13:00'); – ежедневно в 13:00
->twiceDaily(1, 13); – ежедневно дважды в день: дважды в день: в 1:00 и 13:00
->twiceDailyAt(1, 13, 15); - ежедневно в 1:15 и 13:15.
->weekly(); – еженедельно в воскресенье в 00:00
->weeklyOn(1, '8:00'); – еженедельно в понедельник в 8:00
->monthly(); – ежемесячно первого числа в 00:00
->monthlyOn(4, '15:00'); – ежемесячно 4 числа в 15:00
->twiceMonthly(1, 16, '13:00'); – ежемесячно дважды в месяц: 1 и 16 числа в 13:00
->lastDayOfMonth('15:00'); – ежемесячно в последний день месяца в 15:00
->quarterly(); – ежеквартально в первый день в 00:00
->quarterlyOn(4, '14:00'); - ежеквартально в 4-й день в 14:00.
->yearly(); – ежегодно в первый день в 00:00
->yearlyOn(6, 1, '17:00'); – ежегодно в июне первого числа в 17:00
->timezone('America/New_York'); Установить часовой пояс для задачи

Эти методы можно комбинировать с дополнительными ограничениями для создания еще более точных расписаний, которые выполняются только в определенные дни недели. Например, вы можете запланировать выполнение команды еженедельно в понедельник:

// Запускаем раз в неделю в понедельник в 13:00 ...
$schedule->call(function () {
    // ...
})->weekly()->mondays()->at('13:00');

// Запускаем по будням ежечасно с 8 утра до 5 вечера ...
$schedule->command('foo')
          ->weekdays()
          ->hourly()
          ->timezone('America/Chicago')
          ->between('8:00', '17:00');

Список дополнительных ограничений расписания можно найти ниже:

Метод Описание
->weekdays(); Ограничить выполнение задачи рабочими днями
->weekends(); – выходными днями
->sundays(); – воскресным днем
->mondays(); – понедельником
->tuesdays(); – вторником
->wednesdays(); – средой
->thursdays(); – четвергом
->fridays(); – пятницей
->saturdays(); – субботой
->days(array|mixed); – определенными днями
->between($startTime, $endTime); – временными интервалами начала и окончания
->unlessBetween($startTime, $endTime); – через исключение временных интервалов начала и окончания
->when(Closure); – на основе истинности результата выполненного замыкания
->environments($env); – окружением выполнения

Дневные ограничения

Метод days можно использовать для ограничения выполнения задачи определенными днями недели. Например, вы можете запланировать выполнение команды ежечасно по воскресеньям и средам:

$schedule->command('emails:send')
                ->hourly()
                ->days([0, 3]);

В качестве альтернативы вы можете использовать константы, доступные в классе Illuminate\Console\Scheduling\Schedule, при указании дней, в которые должна выполняться задача:

use Illuminate\Console\Scheduling\Schedule;

$schedule->command('emails:send')
                ->hourly()
                ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

Ограничения с временными интервалами

Метод between может использоваться для ограничения выполнения задачи в зависимости от времени суток:

$schedule->command('emails:send')
                    ->hourly()
                    ->between('7:00', '22:00');

Точно так же метод unlessBetween может использоваться для исключения определенных периодов времени выполнения задачи:

$schedule->command('emails:send')
                    ->hourly()
                    ->unlessBetween('23:00', '4:00');

Условные ограничения

Метод when может использоваться для ограничения выполнения задачи на основе истинности результата выполненного замыкания. Другими словами, если переданное замыкание возвращает true, то задача будет выполняться до тех пор, пока никакие другие ограничивающие условия не препятствуют ее запуску:

$schedule->command('emails:send')->daily()->when(function () {
    return true;
});

Метод skip можно рассматривать как противоположный методу when. Если метод skip возвращает true, то запланированная задача не будет выполнена:

$schedule->command('emails:send')->daily()->skip(function () {
    return true;
});

При использовании цепочки методов when, запланированная команда будет выполняться только в том случае, если все условия when возвращают значение true.

Ограничения окружения выполнения

Метод environment может использоваться для выполнения задач только в указанных окружениях, согласно определению переменной APP_ENV окружения:

$schedule->command('emails:send')
            ->daily()
            ->environments(['staging', 'production']);

Часовые пояса

Используя метод timezone, вы можете указать, что время запланированной задачи должно интерпретироваться в рамках переданного часового пояса:

$schedule->command('report:generate')
         ->timezone('America/New_York')
         ->at('2:00')

Если вы постоянно назначаете один и тот же часовой пояс для всех запланированных задач, то вы можете определить метод scheduleTimezone в своем классе App\Console\Kernel. Этот метод должен возвращать часовой пояс, назначаемый по умолчанию для всех запланированных задач:

use DateTimeZone;

/**
 * Получить часовой пояс, который должен использоваться по умолчанию для запланированных событий.
 */
protected function scheduleTimezone(): DateTimeZone|string|null
{
    return 'America/Chicago';
}

Помните, что в некоторых часовых поясах используется летнее время. Когда происходит переход на летнее время, ваша запланированная задача может запускаться дважды или даже не запускаться вообще. По этой причине мы рекомендуем по возможности избегать указаний часовых поясов при планировании.

Предотвращение дублирования задач

По умолчанию запланированные задачи будут выполняться, даже если предыдущий экземпляр задачи все еще выполняется. Чтобы предотвратить это, вы можете использовать метод withoutOverlapping:

$schedule->command('emails:send')->withoutOverlapping();

В этом примере команда emails:send Artisan будет запускаться каждую минуту при условии, что она еще не запущена. Метод withoutOverlapping особенно полезен, если у вас есть задачи, которые разнятся по времени выполнения, что не позволяет вам точно предсказать, сколько времени займет текущая задача.

При необходимости вы можете указать, сколько минут должно пройти до окончания блокировки «перекрывающихся» задач. По умолчанию срок блокировки истекает через 24 часа:

$schedule->command('emails:send')->withoutOverlapping(10);

Внутри метод withoutOverlapping использует кэш вашего приложения для получения блокировок. При необходимости вы можете очистить эти блокировки, используя команду Artisan schedule:clear-cache. Обычно это необходимо только в случае, если задача застревает из-за непредвиденной проблемы с сервером.

Выполнение задач на одном сервере

Чтобы использовать этот функционал, ваше приложение должно использовать по умолчанию один из следующих драйверов кеша: database, memcached, dynamodb, или redis. Кроме того, все серверы должны взаимодействовать с одним и тем же центральным сервером кеширования.

Если планировщик вашего приложения работает на нескольких серверах, то вы можете ограничить выполнение запланированного задания только на одном сервере. Например, предположим, что у вас есть запланированная задача, по которой каждую пятницу вечером создается новый отчет. Если планировщик задач работает на трех рабочих серверах, запланированная задача будет запущена на всех трех серверах и трижды сгенерирует отчет. Не очень хорошо!

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

$schedule->command('report:generate')
                ->fridays()
                ->at('17:00')
                ->onOneServer();

Именование заданий одного сервера

Иногда вам может потребоваться запланировать отправку одного и того же задания с разными параметрами, но при этом указать Laravel запускать каждую модификацию задания на одном сервере. Для этого вы можете присвоить каждому определению расписания уникальное имя с помощью метода name:

$schedule->job(new CheckUptime('https://laravel.com'))
            ->name('check_uptime:laravel.com')
            ->everyFiveMinutes()
            ->onOneServer();

$schedule->job(new CheckUptime('https://vapor.laravel.com'))
            ->name('check_uptime:vapor.laravel.com')
            ->everyFiveMinutes()
            ->onOneServer();

Аналогично, для запланированных замыканий также необходимо присвоить имя, если они должны выполняться на одном сервере:

$schedule->call(fn () => User::resetApiRequestCount())
    ->name('reset-api-request-count')
    ->daily()
    ->onOneServer();

Фоновые задачи

По умолчанию, несколько задач, запланированных одновременно, будут выполняться последовательно в соответствии с порядком, которым они определены в вашем методе schedule. Если у вас есть длительные задачи, это может привести к тому, что последующие задачи начнутся намного позже, чем ожидалось. Если вы хотите запускать задачи в фоновом режиме в соответствии с планом, то вы можете использовать метод runInBackground:

$schedule->command('analytics:report')
         ->daily()
         ->runInBackground();

Метод runInBackground может использоваться только при планировании задач с помощью методов command и exec.

Режим технического обслуживания

Запланированные задачи вашего приложения не будут выполняться, когда приложение находится в режиме обслуживания, поскольку мы не хотим, чтобы ваши задачи мешали любому незавершенному процессу обслуживания, выполняющемуся на вашем сервере. Однако, если вы хотите принудительно запустить задачу даже в режиме обслуживания, то используйте метод evenInMaintenanceMode при определении задачи:

$schedule->command('emails:send')->evenInMaintenanceMode();

Запуск планировщика

Теперь, когда мы узнали, как определять планирование задачи, давайте обсудим, как же запускать их на нашем сервере. Команда schedule:run Artisan проанализирует все ваши запланированные задачи и определит, нужно ли их запускать, исходя из текущего времени сервера.

Итак, при использовании планировщика Laravel нам нужно добавить только одну конфигурационную запись cron на наш сервер, которая запускает команду schedule:run каждую минуту. Если вы не знаете, как добавить записи cron на свой сервер, то рассмотрите возможность использования такой службы, как Laravel Forge, которая может управлять записями cron за вас:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Задания с интервалом менее минуты

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

$schedule->call(function () {
    DB::table('recent_users')->delete();
})->everySecond();

Когда в вашем приложении определены задания с интервалом менее минуты, команда schedule:run будет выполняться до конца текущей минуты, а не завершится немедленно. Это позволяет команде вызывать все необходимые задания с интервалом менее минуты в течение минуты.

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

use App\Jobs\DeleteRecentUsers;

$schedule->job(new DeleteRecentUsers)->everyTenSeconds();

$schedule->command('users:delete')->everyTenSeconds()->runInBackground();

Прерывание задач с интервалом менее минуты:

Поскольку команда schedule:run выполняется в течение всей минуты при наличии задач с интервалом менее минуты, вам иногда может потребоваться прервать выполнение команды при развертывании вашего приложения. В противном случае экземпляр команды schedule:run, который уже выполняется, будет продолжать использовать код вашего приложения, развернутого ранее, пока не завершится текущая минута.

Для прерывания выполняющихся schedule:run вы можете добавить команду schedule:interrupt в сценарий развертывания вашего приложения. Эту команду следует вызвать после завершения развертывания вашего приложения:

php artisan schedule:interrupt

Локальный запуск планировщика

Как правило, на локальной машине нет необходимости в добавлении записи cron планировщика. Вместо этого вы можете использовать команду schedule:work Artisan. Эта команда будет работать на переднем плане и вызывать планировщик каждую минуту, пока вы не завершите команду:

php artisan schedule:work

Результат выполнения задачи

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

$schedule->command('emails:send')
         ->daily()
         ->sendOutputTo($filePath);

Если вы хотите добавить результат в указанный файл, то используйте метод appendOutputTo:

$schedule->command('emails:send')
         ->daily()
         ->appendOutputTo($filePath);

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

$schedule->command('report:generate')
         ->daily()
         ->sendOutputTo($filePath)
         ->emailOutputTo('taylor@example.com');

Если вы хотите отправить результат по электронной почте только в том случае, если запланированная (Artisan или системная) команда завершается ненулевым кодом возврата, используйте метод emailOutputOnFailure:

$schedule->command('report:generate')
         ->daily()
         ->emailOutputOnFailure('taylor@example.com');

Методы emailOutputTo, emailOutputOnFailure, sendOutputTo, and appendOutputTo могут использоваться только при планировании задач с помощью методов command и exec.

Хуки выполнения задачи

Используя методы before и after, вы можете указать замыкания, которые будут выполняться до и после выполнения запланированной задачи:

$schedule->command('emails:send')
         ->daily()
         ->before(function () {
             // Задача готова к выполнению ...
         })
         ->after(function () {
             // Задача выполнена ...
         });

Методы onSuccess и onFailure позволяют указать замыкания, которые будут выполняться в случае успешного или неудачного выполнения запланированной задачи. Ошибка означает, что запланированная (Artisan или системная) команда завершилась ненулевым кодом возврата:

$schedule->command('emails:send')
         ->daily()
         ->onSuccess(function () {
             // Задача успешно выполнена ...
         })
         ->onFailure(function () {
             // Не удалось выполнить задачу ...
         });

Если из вашей команды доступен вывод результата, то вы можете получить к нему доступ в ваших хуках after, onSuccess или onFailure, указав тип экземпляра Illuminate\Support\Stringable в качестве аргумента $output замыкания при определении вашего хука:

use Illuminate\Support\Stringable;

$schedule->command('emails:send')
         ->daily()
         ->onSuccess(function (Stringable $output) {
             // Задача успешно выполнена ...
         })
         ->onFailure(function (Stringable $output) {
             // Не удалось выполнить задачу ...
         });

Пингование URL-адресов

Используя методы pingBefore и thenPing, планировщик может автоматически пинговать по-указанному URL до или после выполнения задачи. Этот метод полезен для уведомления внешней службы, такой как Envoyer, о том, что ваша запланированная задача запущена или завершена:

$schedule->command('emails:send')
         ->daily()
         ->pingBefore($url)
         ->thenPing($url);

Методы pingBeforeIf и thenPingIf могут использоваться для пингования по указанному URL, только если переданное условие $condition истинно:

$schedule->command('emails:send')
         ->daily()
         ->pingBeforeIf($condition, $url)
         ->thenPingIf($condition, $url);

Методы pingOnSuccess и pingOnFailure могут использоваться для пингования по-указанному URL только в случае успешного или неудачного выполнения задачи. Ошибка означает, что запланированная (Artisan или системная) команда завершилась ненулевым кодом возврата:

$schedule->command('emails:send')
         ->daily()
         ->pingOnSuccess($successUrl)
         ->pingOnFailure($failureUrl);

Для всех методов пингования требуется библиотека Guzzle HTTP. Guzzle обычно устанавливается во всех новых проектах Laravel по умолчанию, но вы можете вручную установить Guzzle в свой проект с помощью менеджера пакетов Composer, если он был удален:

composer require guzzlehttp/guzzle

События

При необходимости вы можете прослушивать события отправленные планировщиком. Как правило, сопоставления слушателей событий определяются в классе вашего приложения App\Providers\EventServiceProvider:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Console\Events\ScheduledTaskStarting' => [
        'App\Listeners\LogScheduledTaskStarting',
    ],

    'Illuminate\Console\Events\ScheduledTaskFinished' => [
        'App\Listeners\LogScheduledTaskFinished',
    ],

    'Illuminate\Console\Events\ScheduledBackgroundTaskFinished' => [
        'App\Listeners\LogScheduledBackgroundTaskFinished',
    ],

    'Illuminate\Console\Events\ScheduledTaskSkipped' => [
        'App\Listeners\LogScheduledTaskSkipped',
    ],

    'Illuminate\Console\Events\ScheduledTaskFailed' => [
        'App\Listeners\LogScheduledTaskFailed',
    ],
];