MidТеория7 min

Планирование задач (Task Scheduling)

Определение расписаний, параметры частоты, предотвращение перекрытий, выполнение на одном сервере, режим обслуживания, обработка вывода

Планировщик задач Laravel позволяет определять расписание выполнения команд внутри приложения, заменяя множество cron-записей одной. Расписание определяется в методе schedule класса routes/console.php или в App\Console\Kernel.

Настройка планировщика

Для работы планировщика нужна одна cron-запись на сервере:

# Add to crontab (crontab -e)
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

В Laravel 11+ расписание определяется в routes/console.php:

<?php

// routes/console.php

use Illuminate\Support\Facades\Schedule;
use App\Console\Commands\SendDailyReport;
use App\Jobs\CleanUpExpiredTokens;
use App\Models\User;

// Schedule an Artisan command
Schedule::command('inspire')->hourly();

// Schedule with full command signature
Schedule::command('emails:send --force')->daily();

// Schedule a job
Schedule::job(new CleanUpExpiredTokens)->daily();

// Schedule a closure
Schedule::call(function () {
    User::where('trial_ends_at', '<', now())
        ->update(['is_active' => false]);
})->daily();

// Schedule a shell command
Schedule::exec('node /home/forge/script.js')->daily();

Параметры частоты

Laravel предоставляет множество методов для определения частоты выполнения.

use Illuminate\Support\Facades\Schedule;

// Basic frequencies
Schedule::command('report:generate')->everySecond();
Schedule::command('report:generate')->everyTwoSeconds();
Schedule::command('report:generate')->everyFiveSeconds();
Schedule::command('report:generate')->everyTenSeconds();
Schedule::command('report:generate')->everyFifteenSeconds();
Schedule::command('report:generate')->everyTwentySeconds();
Schedule::command('report:generate')->everyThirtySeconds();

Schedule::command('report:generate')->everyMinute();
Schedule::command('report:generate')->everyTwoMinutes();
Schedule::command('report:generate')->everyThreeMinutes();
Schedule::command('report:generate')->everyFourMinutes();
Schedule::command('report:generate')->everyFiveMinutes();
Schedule::command('report:generate')->everyTenMinutes();
Schedule::command('report:generate')->everyFifteenMinutes();
Schedule::command('report:generate')->everyThirtyMinutes();

Schedule::command('report:generate')->hourly();
Schedule::command('report:generate')->hourlyAt(17); // At :17 past the hour
Schedule::command('report:generate')->everyOddHour();
Schedule::command('report:generate')->everyTwoHours();
Schedule::command('report:generate')->everyThreeHours();
Schedule::command('report:generate')->everyFourHours();
Schedule::command('report:generate')->everySixHours();

Schedule::command('report:generate')->daily();
Schedule::command('report:generate')->dailyAt('13:00');
Schedule::command('report:generate')->twiceDaily(1, 13); // At 1:00 & 13:00
Schedule::command('report:generate')->twiceDailyAt(1, 13, 15); // At 1:15 & 13:15

Schedule::command('report:generate')->weekly();
Schedule::command('report:generate')->weeklyOn(1, '8:00'); // Monday at 8:00

Schedule::command('report:generate')->monthly();
Schedule::command('report:generate')->monthlyOn(4, '15:00'); // 4th at 15:00
Schedule::command('report:generate')->twiceMonthly(1, 16, '13:00');
Schedule::command('report:generate')->lastDayOfMonth('17:00');

Schedule::command('report:generate')->quarterly();
Schedule::command('report:generate')->quarterlyOn(4, '14:00');

Schedule::command('report:generate')->yearly();
Schedule::command('report:generate')->yearlyOn(6, 1, '17:00'); // June 1st

Ограничения дня

use Illuminate\Support\Facades\Schedule;

// Day constraints
Schedule::command('report:generate')
    ->daily()
    ->weekdays();         // Mon-Fri

Schedule::command('report:generate')
    ->daily()
    ->weekends();         // Sat-Sun

Schedule::command('report:generate')
    ->daily()
    ->sundays();          // Only Sundays

Schedule::command('report:generate')
    ->daily()
    ->mondays();          // Only Mondays

Schedule::command('report:generate')
    ->daily()
    ->tuesdays();

Schedule::command('report:generate')
    ->daily()
    ->wednesdays();

Schedule::command('report:generate')
    ->daily()
    ->thursdays();

Schedule::command('report:generate')
    ->daily()
    ->fridays();

Schedule::command('report:generate')
    ->daily()
    ->saturdays();

// Multiple day constraints
Schedule::command('report:generate')
    ->daily()
    ->days([0, 3, 5]);   // Sun, Wed, Fri

Временные ограничения

use Illuminate\Support\Facades\Schedule;

// Time constraints
Schedule::command('report:generate')
    ->hourly()
    ->between('8:00', '17:00');    // Only between 8am-5pm

Schedule::command('report:generate')
    ->hourly()
    ->unlessBetween('23:00', '4:00');  // Not between 11pm-4am

Cron-выражения

use Illuminate\Support\Facades\Schedule;

// Raw cron expression
Schedule::command('report:generate')
    ->cron('0 */6 * * *');  // Every 6 hours

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

use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->daily()
    ->timezone('Europe/Moscow');

// Or set default timezone for all tasks
// In App\Providers\AppServiceProvider
protected function scheduleTimezone(): \DateTimeZone
{
    return new \DateTimeZone('Europe/Moscow');
}

Предотвращение перекрытий

Метод withoutOverlapping() гарантирует, что одновременно выполняется только один экземпляр задачи.

use Illuminate\Support\Facades\Schedule;

// Default lock expiration: 24 hours
Schedule::command('report:generate')
    ->hourly()
    ->withoutOverlapping();

// Custom lock expiration (in minutes)
Schedule::command('report:generate')
    ->hourly()
    ->withoutOverlapping(30); // Lock expires after 30 minutes

Блокировка освобождается после завершения задачи или по истечении таймаута. По умолчанию используется кэш-драйвер для хранения блокировок.

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

При использовании нескольких серверов задача по умолчанию выполняется на каждом. Метод onOneServer() гарантирует выполнение на одном сервере.

use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->daily()
    ->onOneServer();

// Requires a centralized cache driver: memcached, redis, dynamodb, database
// Does NOT work with the 'file' cache driver

Важно: onOneServer() требует централизованный кэш-драйвер (redis, memcached, database). Файловый кэш не подходит, так как каждый сервер имеет свою файловую систему.

Условное выполнение

use Illuminate\Support\Facades\Schedule;

// when() - execute only if true
Schedule::command('emails:send')
    ->daily()
    ->when(function () {
        return app()->environment('production');
    });

// skip() - skip if true
Schedule::command('emails:send')
    ->daily()
    ->skip(function () {
        return app()->isDownForMaintenance();
    });

// environments() - only on specified environments
Schedule::command('emails:send')
    ->daily()
    ->environments(['staging', 'production']);

Режим обслуживания (Maintenance Mode)

По умолчанию задачи не выполняются в режиме обслуживания. Метод evenInMaintenanceMode() позволяет принудительно выполнять задачу.

use Illuminate\Support\Facades\Schedule;

// Will run even during maintenance mode
Schedule::command('heartbeat:check')
    ->everyMinute()
    ->evenInMaintenanceMode();

Обработка вывода

use Illuminate\Support\Facades\Schedule;

// Send output to a file
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo('/var/log/schedule/report.log');

// Append output to a file
Schedule::command('report:generate')
    ->daily()
    ->appendOutputTo('/var/log/schedule/report.log');

// Email output (only if there is output)
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo('/var/log/schedule/report.log')
    ->emailOutputTo('[email protected]');

// Email output only on failure
Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('[email protected]');

Хуки (Task Hooks)

use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->daily()
    ->before(function () {
        // Before the task runs
        \Log::info('Starting report generation');
    })
    ->after(function () {
        // After the task completes
        \Log::info('Report generation finished');
    })
    ->onSuccess(function () {
        // Only if the task succeeds
        \Log::info('Report generated successfully');
    })
    ->onFailure(function () {
        // Only if the task fails
        \Log::error('Report generation failed');
    });

Ping URL (Webhooks)

use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->daily()
    ->pingBefore('https://healthcheck.io/start/abc123')
    ->thenPing('https://healthcheck.io/complete/abc123')
    ->pingOnSuccess('https://healthcheck.io/success/abc123')
    ->pingOnFailure('https://healthcheck.io/failure/abc123');

// Ping with conditions
Schedule::command('report:generate')
    ->daily()
    ->pingBeforeIf($condition, $url)
    ->thenPingIf($condition, $url);

Sub-Minute Scheduling

Laravel 11 поддерживает выполнение задач чаще, чем раз в минуту.

use Illuminate\Support\Facades\Schedule;

// Run every second
Schedule::call(function () {
    // Check for new messages
})->everySecond();

// Run every 10 seconds
Schedule::command('queue:monitor redis:default --max=100')
    ->everyTenSeconds();

// Run every 15 seconds
Schedule::job(new CheckServiceHealth)
    ->everyFifteenSeconds()
    ->withoutOverlapping();

Группировка задач

use Illuminate\Support\Facades\Schedule;

// Apply common settings to a group
Schedule::command('analytics:daily')->daily()->onOneServer()->withoutOverlapping();
Schedule::command('analytics:weekly')->weekly()->onOneServer()->withoutOverlapping();
Schedule::command('analytics:monthly')->monthly()->onOneServer()->withoutOverlapping();

Artisan-команды для мониторинга

# List all scheduled tasks
php artisan schedule:list

# Run the scheduler manually
php artisan schedule:run

# Run the scheduler in the foreground (development)
php artisan schedule:work

# Test a specific scheduled command
php artisan schedule:test

# Interrupt currently running scheduled tasks
php artisan schedule:interrupt

Практический пример: полная конфигурация

<?php

// routes/console.php

use Illuminate\Support\Facades\Schedule;

// Daily maintenance tasks
Schedule::command('sanctum:prune-expired --hours=24')
    ->daily()
    ->onOneServer()
    ->withoutOverlapping()
    ->appendOutputTo(storage_path('logs/schedule.log'));

// Hourly report generation
Schedule::command('reports:generate --type=hourly')
    ->hourly()
    ->between('6:00', '22:00')
    ->weekdays()
    ->onOneServer()
    ->withoutOverlapping(30)
    ->onSuccess(function () {
        \Log::channel('schedule')->info('Hourly report generated');
    })
    ->onFailure(function () {
        \Log::channel('schedule')->error('Hourly report failed');
    });

// Queue monitoring
Schedule::command('queue:monitor redis:default,redis:high --max=100')
    ->everyFiveMinutes()
    ->environments(['production']);

// Database cleanup
Schedule::command('model:prune --model=App\\Models\\ActivityLog')
    ->daily()
    ->at('03:00')
    ->onOneServer()
    ->evenInMaintenanceMode();

// Health checks
Schedule::call(function () {
    $services = ['redis', 'database', 'queue'];
    foreach ($services as $service) {
        // Perform health check
    }
})->everyMinute()
  ->onOneServer()
  ->pingOnFailure('https://healthcheck.io/fail/abc123');

// Weekly digest
Schedule::command('emails:weekly-digest')
    ->weeklyOn(1, '9:00') // Monday at 9:00 AM
    ->timezone('Europe/Moscow')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('[email protected]');

Проверь себя

Какая cron-запись нужна на сервере для работы планировщика Laravel?

Какой кэш-драйвер НЕ подходит для метода onOneServer()?

Метод between('8:00', '17:00') в планировщике означает:

Что происходит с запланированными задачами, когда приложение в режиме обслуживания (maintenance mode)?

Какой метод гарантирует, что запланированная задача выполнится только на ОДНОМ сервере в кластере?

Связанные темы