タスクスケジューリング
はじめに
過去には、サーバーでスケジュール設定が必要なタスクごとにcron設定エントリを記述していたかもしれません。しかし、これはすぐに面倒になる可能性があります。なぜなら、タスクスケジュールがソース管理されなくなり、既存のcronエントリを表示したり追加したりするためにサーバーにSSHで接続する必要があるからです。
Laravelのコマンドスケジューラは、サーバー上でスケジュールされたタスクを管理する新しいアプローチを提供します。スケジューラを使用すると、Laravelアプリケーション内でコマンドスケジュールを流暢かつ表現豊かに定義することができます。スケジューラを使用すると、サーバーには1つのcronエントリしか必要ありません。通常、タスクスケジュールはアプリケーションの routes/console.php
ファイルに定義されます。
スケジュールの定義
アプリケーションの routes/console.php
ファイルにすべてのスケジュールされたタスクを定義することができます。始めるために、例を見てみましょう。この例では、毎日真夜中に呼び出されるクロージャをスケジュールします。クロージャ内では、テーブルをクリアするためにデータベースクエリを実行します:
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
クロージャを使用してスケジュールする他に、呼び出し可能オブジェクト をスケジュールすることもできます。呼び出し可能オブジェクトは、__invoke
メソッドを含むシンプルなPHPクラスです。
Schedule::call(new DeleteRecentUsers)->daily();
routes/console.php
ファイルをコマンド定義のみに予約したい場合は、アプリケーションのbootstrap/app.php
ファイルでwithSchedule
メソッドを使用してスケジュールされたタスクを定義できます。このメソッドは、スケジューラのインスタンスを受け取るクロージャを受け入れます:
use Illuminate\Console\Scheduling\Schedule;
->withSchedule(function (Schedule $schedule) {
$schedule->call(new DeleteRecentUsers)->daily();
})
スケジュールされたタスクと次回実行予定時刻の概要を表示したい場合は、schedule:list
Artisanコマンドを使用できます:
php artisan schedule:list
Artisanコマンドのスケジュール
クロージャをスケジュールするだけでなく、Artisanコマンドやシステムコマンドをスケジュールすることもできます。たとえば、command
メソッドを使用して、コマンドの名前またはクラスを使用してArtisanコマンドをスケジュールできます。
コマンドのクラス名を使用してArtisanコマンドをスケジュールする場合、コマンドが呼び出されるときにコマンドに提供する追加のコマンドライン引数の配列を渡すことができます:
use App\Console\Commands\SendEmailsCommand;
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send Taylor --force')->daily();
Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
Artisan Closureコマンドのスケジュール
クロージャで定義されたArtisanコマンドをスケジュールしたい場合は、コマンドの定義の後にスケジュール関連のメソッドをチェーンすることができます:
Artisan::command('delete:recent-users', function () {
DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();
クロージャコマンドに引数を渡す必要がある場合は、schedule
メソッドにそれらを提供できます:
Artisan::command('emails:send {user} {--force}', function ($user) {
// ...
})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();
キューに入れたジョブのスケジュール
job
メソッドを使用して、キューに入れたジョブをスケジュールすることができます。このメソッドは、ジョブをキューに入れるためにクロージャを定義するためにcall
メソッドを使用せずに便利な方法を提供します:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new Heartbeat)->everyFiveMinutes();
job
メソッドには、ジョブをキューに入れるために使用するキュー名とキュー接続を指定するためのオプションの第2および第3引数を提供できます:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
// Dispatch the job to the "heartbeats" queue on the "sqs" connection...
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
exec
メソッドを使用して、オペレーティングシステムにコマンドを発行できます:
use Illuminate\Support\Facades\Schedule;
Schedule::exec('node /home/forge/script.js')->daily();
スケジュール頻度オプション
指定された間隔でタスクを実行する方法のいくつかの 例をすでに見てきました。ただし、タスクに割り当てることができる多くのタスクスケジュール頻度があります:
Method | Description |
---|---|
->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(); | タスクを毎月1日00:00に実行 |
->monthlyOn(4, '15:00'); | タスクを毎月4日15:00に実行 |
->twiceMonthly(1, 16, '13:00'); | タスクを毎月1日と16日13:00に実行 |
->lastDayOfMonth('15:00'); | 月末日15:00にタスクを実行 |
->quarterly(); | 毎四半期1日00:00にタスクを実行 |
->quarterlyOn(4, '14:00'); | 毎四半期4日14:00にタスクを実行 |
->yearly(); | 毎年1日00:00にタスクを実行 |
->yearlyOn(6, 1, '17:00'); | 毎年6月1日17:00にタスクを実行 |
->timezone('America/New_York'); | タスクのタイムゾーンを設定します |
これらのメソッドは、さらに細かく調整されたスケジュールを作成するために追加の制約と組み合わせることができます。たとえば、コマンドを毎週月曜日に実行するようにスケジュールすることができます:
use Illuminate\Support\Facades\Schedule;
// Run once per week on Monday at 1 PM...
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// Run hourly from 8 AM to 5 PM on weekdays...
Schedule::command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
以下は、追加のスケジュール制約のリストです:
Method | Description |
---|---|
->weekdays(); | タスクを平日に制限する |
->weekends(); | タスクを週末に制限する |
->sundays(); | タスクを日曜日に制限する |
->mondays(); | タスクを月曜日に制限する |
->tuesdays(); | タスクを火曜日に制限する |
->wednesdays(); | タスクを水曜日に制限する |
->thursdays(); | タスクを木曜 日に制限する |
->fridays(); | タスクを金曜日に制限する |
->saturdays(); | タスクを土曜日に制限する |
->days(array|mixed); | タスクを特定の日に制限する |
->between($startTime, $endTime); | タスクを開始時間と終了時間の間で実行する |
->unlessBetween($startTime, $endTime); | タスクを開始時間と終了時間の間で実行しないように制限する |
->when(Closure); | 真偽テストに基づいてタスクを制限する |
->environments($env); | 特定の環境にタスクを制限する |
日付制約
days
メソッドを使用して、タスクの実行を特定の曜日に制限することができます。たとえば、コマンドを日曜日と水曜日に毎時実行するようにスケジュールすることができます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->hourly()
->days([0, 3]);
また、タスクが実行される日を定義する際に、Illuminate\Console\Scheduling\Schedule
クラスで利用可能な定数を使用することもできます:
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;
Facades\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
を返す場合にのみ実行されます。
環境制約
environments
メソッドは、指定された環境(APP_ENV
環境変数で定義される)でのみタスクを実行するために使用できます:
Schedule::command('emails:send')
->daily()
->environments(['staging', 'production']);
タイムゾーン
timezone
メソッドを使用すると、スケジュールされたタスクの時間が特定のタイムゾーン内で解釈されるように指定できます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->timezone('America/New_York')
->at('2:00')
すべてのスケジュールされたタスクに同じタイムゾーンを繰り返し割り当てる場合は、アプリケーションのapp
構成ファイル内でschedule_timezone
オプションを定義することで、すべてのスケジュールに割り当てるべきタイムゾーンを指定できます:
'timezone' => env('APP_TIMEZONE', 'UTC'),
'schedule_timezone' => 'America/Chicago',
一部のタイムゾーンは夏時間を利用しています。夏時間の変更が発生すると、スケジュールされたタスクが2回実行されるか、まったく実行されないことがあります。そのため、可能な限りタイムゾーンのスケジューリングを避けることをお勧めします。
タスクの重複を防ぐ
デフォルトでは、前のタスクのインスタンスがまだ実行中であっても、スケジュールされたタスクは実行されます。これを防ぐには、withoutOverlapping
メソッドを使用できます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
この例では、emails:send
Artisanコマンドは、すでに実行中でない場合に毎分実行されます。withoutOverlapping
メソッドは、実行時間が大幅に異なるタスクがある場合に特に便利であり、特定のタスクがどれくらいかかるかを正確に予測できない場合に役立ちます。
必要に応じて、「重複しない」ロックが期限切れになるまでの経過分数を指定できます。デフォルトでは、ロックは24時間後に期限切れになります:
Schedule::command('emails:send')->withoutOverlapping(10);
裏側では、withoutOverlapping
メソッドはアプリケーションのキャッシュを利用してロックを取得します。必要に応じて、これらのキャッシュロックを schedule:clear-cache
Artisan コマンドを使用してクリアできます。これは、予期しないサーバーの問題によってタスクがスタックした場合にのみ必要とされます。
1つのサーバーでタスクを実行する
この機能を利用するには、アプリケーションが database
、memcached
、dynamodb
、または redis
キャッシュドライバをアプリケーションのデフォルトキャッシュドライバとして使用している必要があります。さらに、すべてのサーバーが同じ中央キャッシュサーバーと通信している必要があります。
アプリケーションのスケジューラが複数のサーバーで実行されている場合、スケジュールされたジョブを1つのサーバーでのみ実行するように制限できます。たとえば、毎週金曜日の夜に新しいレポートを生成するスケジュールされたタスクがあるとします。タスクスケジューラが3つのワーカーサーバーで実行されている場合、スケジュールされたタスクは3つのサーバーすべてで実行され、レポートが3回生成されます。これはよくありません!
タスクが1つのサーバーでのみ実行されるように指定するには、スケジュールされたタスクを定義する際に onOneServer
メソッドを使用します。タスクを最初に取得したサーバーがジョブに対してアトミックロックを確 保し、他のサーバーが同時に同じタスクを実行するのを防ぎます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
ユニークなジョブの名前付け
同じジョブを異なるパラメータでディスパッチするようにスケジュールする必要がある場合がありますが、それでも Laravel に各ジョブのパーミュテーションを1つのサーバーで実行するよう指示する必要があります。これを達成するには、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();
同様に、スケジュールされたクロージャは1つのサーバーで実行されるように意図されている場合は名前を割り当てる必要があります:
Schedule::call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();
バックグラウンドタスク
デフォルトでは、同時にスケジュールされた複数のタスクは、schedule
メソッドで定義された順序に基づいて順次実行されます。長時間実行されるタスクがある場合、これにより後続のタスクが予想よりもはるかに遅れて開始される可能性があります。すべてのタスクを同時に実行するためにバックグ ラウンドで実行したい場合は、runInBackground
メソッドを使用できます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('analytics:report')
->daily()
->runInBackground();
runInBackground
メソッドは、タスクを command
メソッドおよび exec
メソッドを使用してスケジュールする場合にのみ使用できます。
メンテナンスモード
アプリケーションのスケジュールされたタスクは、アプリケーションがmaintenance modeにあるときには実行されません。サーバーで実行中の未完了のメンテナンス作業に干渉しないようにするためです。ただし、メンテナンスモードでもタスクを強制的に実行したい場合は、タスクを定義する際に evenInMaintenanceMode
メソッドを呼び出すことができます:
Schedule::command('emails:send')->evenInMaintenanceMode();
スケジューラの実行
スケジュールされたタスクの定義方法を学んだので、実際にサーバーでそれらを実行する方法について説明しましょう。schedule:run
Artisan コマンドは、すべてのスケジュールされたタスクを評価し、サーバーの現在時刻に基づいて実行する必要があるかどうかを決定します。
したがって、Laravelのスケジューラを使用する場合、サーバーに schedule:run
コマンドを毎分実行する単一の cron 設定エントリを追加するだけで済みます。サーバーに cron エントリを追加する方法がわからない場合は、Laravel Forgeなどのサービスを使用して cron エントリを管理できます:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
サブ分スケジュールされたタスク
ほとんどのオペレーティングシステムでは、cron ジョブは最大で1分ごとに実行されるように制限されています。ただし、Laravelのスケジューラを使用すると、1秒ごとなど、より頻繁な間隔でタスクをスケジュールすることができます:
use Illuminate\Support\Facades\Schedule;
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
コマンドは、サブ分単位のタスクが定義されている場合、呼び出し時の1分間すべて実行されるため、アプリケーションをデプロイする際にコマンドを中断する必要がある場合があります。そうしないと、すでに実行中のschedule:run
コマンドのインスタンスは、現在の分が終了するまで、アプリケーションの以前にデプロイされたコードを引き続き使用します。
進行中のschedule:run
呼び出しを中断するには、アプリケーションのデプロイメントスクリプトにschedule:interrupt
コマンドを追加することができます。このコマンドは、アプリケーションのデプロイが完了した後に呼び出す必要があります:
php artisan schedule:interrupt
ローカルでスケジューラを実行する
通常、ローカルの開発マシンにスケジューラのcronエントリを追加する必要はありません。代わりに、schedule:work
Artisanコマンドを使用できます。このコマンドは前面で実行され、1分ごとにスケジューラを呼び出し続けます。
php artisan schedule:work
タスクの出力
Laravelスケジューラは、スケジュールされたタスクによって生成された出力を処理するためのいくつかの便利なメソッドを提供しています。まず、sendOutputTo
メソッドを使用して、出力を後で検査するためのファイルに送信できます:
use Illuminate\Support\Facades\Schedule;
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
、およびappendOutputTo
メソッドは、command
およびexec
メソッドにのみ適用されます。
タスクフック
before
およびafter
メソッドを使用して、ス ケジュールされたタスクの実行前および実行後に実行するコードを指定できます:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// The task is about to execute...
})
->after(function () {
// The task has executed...
});
onSuccess
およびonFailure
メソッドを使用すると、スケジュールされたタスクが成功または失敗した場合に実行するコードを指定できます。失敗は、スケジュールされたArtisanコマンドまたはシステムコマンドがゼロ以外の終了コードで終了したことを示します:
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// The task succeeded...
})
->onFailure(function () {
// The task failed...
});
コマンドから出力が利用可能な場合、after
、onSuccess
、またはonFailure
フックで、Illuminate\Support\Stringable
インスタンスを$output
引数のクロージャ定義の型ヒントとして指定することで、その出力にアクセスできます:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// The task succeeded...
})
->onFailure(function (Stringable $output) {
// The task failed...
});
URLのPing
pingBefore
およびthenPing
メソッドを使用すると、スケジュールされたタスクの実行前または実行後に指定されたURLに自動的にPingを送信できます。このメソッドは、スケジュールされたタスクの開始または完了を外部サービス(たとえばEnvoyer)に通知するのに便利です:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
pingBeforeIf
およびthenPingIf
メソッドを使用すると、指定された条件がtrue
の場合にのみ、指定されたURLにPingを送信できます:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
pingOnSuccess
およびpingOnFailure
メソッドを使用すると、タスクが成功または失敗した場合にのみ、指定されたURLにPingを送信できます。失敗は、スケジュールされたArtisanコマンドまたはシステムコマンドがゼロ以外の終了コードで終了したことを示します:
Schedule::command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
イベント
Laravelはスケジューリングプロセス中にさまざまなイベントをディスパッチします。以下のイベントのいずれかに対してリスナーを定義できます:
イベント名 |
---|
Illuminate\Console\Events\ScheduledTaskStarting |
Illuminate\Console\Events\ScheduledTaskFinished |
Illuminate\Console\Events\ScheduledBackgroundTaskFinished |
Illuminate\Console\Events\ScheduledTaskSkipped |
Illuminate\Console\Events\ScheduledTaskFailed |