任務排程
簡介
以前,我們需要在伺服器上為每個需要排程執行的任務撰寫 Cron 設定。不過,手動設定 Cron 很快就會變得很麻煩,因為這些排程任務不在版本控制裡面,而且我們必須要 SSH 連進伺服器上才能檢視現有的 Cron 項目以及新增新項目。
Laravel's command scheduler offers a fresh approach to managing scheduled tasks on your server. The scheduler allows you to fluently and expressively define your command schedule within your Laravel application itself. When using the scheduler, only a single cron entry is needed on your server. Your task schedule is typically defined in your application's routes/console.php
file.
定義排程
You may define all of your scheduled tasks in your application's routes/console.php
file. To get started, let's take a look at an example. In this example, we will schedule a closure to be called every day at midnight. Within the closure we will execute a database query to clear a table:
1<?php23use Illuminate\Support\Facades\DB;4use Illuminate\Support\Facades\Schedule;56Schedule::call(function () {7 DB::table('recent_users')->delete();8})->daily();
1<?php23use Illuminate\Support\Facades\DB;4use Illuminate\Support\Facades\Schedule;56Schedule::call(function () {7 DB::table('recent_users')->delete();8})->daily();
除了使用閉包來排程以外,也可以排程執行 可 Invoke 的物件。可 Invoke 的物件只是一個包含 __invoke
方法的普通 PHP 類別:
1Schedule::call(new DeleteRecentUsers)->daily();
1Schedule::call(new DeleteRecentUsers)->daily();
If you prefer to reserve your routes/console.php
file for command definitions only, you may use the withSchedule
method in your application's bootstrap/app.php
file to define your scheduled tasks. This method accepts a closure that receives an instance of the scheduler:
1use Illuminate\Console\Scheduling\Schedule;23->withSchedule(function (Schedule $schedule) {4 $schedule->call(new DeleteRecentUsers)->daily();5})
1use Illuminate\Console\Scheduling\Schedule;23->withSchedule(function (Schedule $schedule) {4 $schedule->call(new DeleteRecentUsers)->daily();5})
若想檢視目前排程任務的概覽,以及各個任務下次排定的執行時間,可使用 schedule:list
Artisan 指令:
1php artisan schedule:list
1php artisan schedule:list
排程執行 Artisan 指令
除了排程執行閉包外,也可以排程執行 Artisan 指令與系統指令。舉例來說,我們可以使用 command
方法來使用指令的名稱或類別名稱來排程執行 Artisan 指令。
若使用指令的類別名稱來排程執行 Artisan 指令時,可傳入一組包含額外指令列引數的陣列,在叫用該指令時會提供這些引數:
1use App\Console\Commands\SendEmailsCommand;2use Illuminate\Support\Facades\Schedule;34Schedule::command('emails:send Taylor --force')->daily();56Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
1use App\Console\Commands\SendEmailsCommand;2use Illuminate\Support\Facades\Schedule;34Schedule::command('emails:send Taylor --force')->daily();56Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
Scheduling Artisan Closure Commands
If you want to schedule an Artisan command defined by a closure, you may chain the scheduling related methods after the command's definition:
1Artisan::command('delete:recent-users', function () {2 DB::table('recent_users')->delete();3})->purpose('Delete recent users')->daily();
1Artisan::command('delete:recent-users', function () {2 DB::table('recent_users')->delete();3})->purpose('Delete recent users')->daily();
If you need to pass arguments to the closure command, you may provide them to the schedule
method:
1Artisan::command('emails:send {user} {--force}', function ($user) {2 // ...3})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();
1Artisan::command('emails:send {user} {--force}', function ($user) {2 // ...3})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();
排程執行放入佇列的 Job
可使用 job
方法來排程執行放入佇列的 Job。該方法提供了一個方便的方法能讓我們能排程執行放入佇列的 Job,而不需使用 call
方法來定義將該 Job 放入佇列的閉包:
1use App\Jobs\Heartbeat;2use Illuminate\Support\Facades\Schedule;34Schedule::job(new Heartbeat)->everyFiveMinutes();
1use App\Jobs\Heartbeat;2use Illuminate\Support\Facades\Schedule;34Schedule::job(new Heartbeat)->everyFiveMinutes();
job
還有可選的第二個引數與第三個引數,可用來指定該 Job 要使用的佇列名稱與佇列連線:
1use App\Jobs\Heartbeat;2use Illuminate\Support\Facades\Schedule;34// Dispatch the job to the "heartbeats" queue on the "sqs" connection...5Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
1use App\Jobs\Heartbeat;2use Illuminate\Support\Facades\Schedule;34// Dispatch the job to the "heartbeats" queue on the "sqs" connection...5Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
排程執行 Shell 指令
可使用 exec
指令來在作業系統上執行指令:
1use Illuminate\Support\Facades\Schedule;23Schedule::exec('node /home/forge/script.js')->daily();
1use Illuminate\Support\Facades\Schedule;23Schedule::exec('node /home/forge/script.js')->daily();
排程的頻率選項
我們已經看到了一些在指定間隔間執行任務的範例。不過,還有其他許多用來指派給任務的排程頻率:
1use Illuminate\Support\Facades\Schedule;23// Run once per week on Monday at 1 PM...4Schedule::call(function () {5 // ...6})->weekly()->mondays()->at('13:00');78// Run hourly from 8 AM to 5 PM on weekdays...9Schedule::command('foo')10 ->weekdays()11 ->hourly()12 ->timezone('America/Chicago')13 ->between('8:00', '17:00');
1use Illuminate\Support\Facades\Schedule;23// Run once per week on Monday at 1 PM...4Schedule::call(function () {5 // ...6})->weekly()->mondays()->at('13:00');78// Run hourly from 8 AM to 5 PM on weekdays...9Schedule::command('foo')10 ->weekdays()11 ->hourly()12 ->timezone('America/Chicago')13 ->between('8:00', '17:00');
下表中列出了其他額外的排程條件限制:
「日」的條件限制
可使用 days
方法來限制任務只在每週的某幾天時執行。舉例來說,我們可以排程執行每週日至週三的每小時執行某個指令:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->hourly()5 ->days([0, 3]);
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->hourly()5 ->days([0, 3]);
或者,我們也可以使用 Illuminate\Console\Scheduling\Schedule
類別中所提供的常數來定義任務要在哪幾天執行:
1use Illuminate\Support\Facades;2use Illuminate\Console\Scheduling\Schedule;34Facades\Schedule::command('emails:send')5 ->hourly()6 ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
1use Illuminate\Support\Facades;2use Illuminate\Console\Scheduling\Schedule;34Facades\Schedule::command('emails:send')5 ->hourly()6 ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
時間區間的條件限制
between
方法可使用一天中指定的時間來限制任務的執行:
1Schedule::command('emails:send')2 ->hourly()3 ->between('7:00', '22:00');
1Schedule::command('emails:send')2 ->hourly()3 ->between('7:00', '22:00');
類似地,unlessBetween
方法可用讓任務在某一段時間內不要執行:
1Schedule::command('emails:send')2 ->hourly()3 ->unlessBetween('23:00', '4:00');
1Schedule::command('emails:send')2 ->hourly()3 ->unlessBetween('23:00', '4:00');
真值條件測試的條件顯示
when
方法可用來依據給定真值測試的結果來限制任務的執行。換句話說,若給定的閉包回傳 true
,除非有其他的條件阻止該任務執行,否則就會執行該任務:
1Schedule::command('emails:send')->daily()->when(function () {2 return true;3});
1Schedule::command('emails:send')->daily()->when(function () {2 return true;3});
skip
方法相當於 when
的相反。若 skip
方法回傳 true
,則排程執行的任務將不被執行:
1Schedule::command('emails:send')->daily()->skip(function () {2 return true;3});
1Schedule::command('emails:send')->daily()->skip(function () {2 return true;3});
串接使用 when
方法時,只有在 when
條件為 true
時排程的任務才會被執行。
環境的條件限制
使用 environments
方法即可讓該方法只在給定的環境上執行 (即 APP_ENV
環境變數 中所定義的):
1Schedule::command('emails:send')2 ->daily()3 ->environments(['staging', 'production']);
1Schedule::command('emails:send')2 ->daily()3 ->environments(['staging', 'production']);
時區
使用 timezone
方法,就可以指定要使用給定的時區來解析排程任務的時間:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('report:generate')4 ->timezone('America/New_York')5 ->at('2:00')
1use Illuminate\Support\Facades\Schedule;23Schedule::command('report:generate')4 ->timezone('America/New_York')5 ->at('2:00')
If you are repeatedly assigning the same timezone to all of your scheduled tasks, you can specify which timezone should be assigned to all schedules by defining a schedule_timezone
option within your application's app
configuration file:
1'timezone' => env('APP_TIMEZONE', 'UTC'),23'schedule_timezone' => 'America/Chicago',
1'timezone' => env('APP_TIMEZONE', 'UTC'),23'schedule_timezone' => 'America/Chicago',
請注意,某些時區會使用日光節約時間。若發生日光節約時間,則某些排程任務可能會執行兩次、甚至是執行多次。因此,我們建議儘可能不要在排程上設定時區。
避免任務重疊
預設情況下,就算之前的任務實體還在執行,也會繼續執行排程的任務。若要避免任務重疊,可使用 withoutOverlapping
方法:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')->withoutOverlapping();
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')->withoutOverlapping();
在這個範例中,若目前沒有在執行 emails:send
Artisan 指令,則該指令每分鐘都會執行。若任務會執行非常久的時間,因而無法預期任務要執行多久,就適合使用 withoutOverlapping
方法。
若有需要,可指定「withoutOverlapping」的 Lock 最少要過多久才算逾期。預設情況下,該 Lock 會在 24 小時候逾期:
1Schedule::command('emails:send')->withoutOverlapping(10);
1Schedule::command('emails:send')->withoutOverlapping(10);
其實,withoutOverlapping
方法會使用專案的 Cache 來取得鎖定。若有需要的話,可以使用 schedule:clear-cache
Artisan 指令來清除這些快取鎖定。通常只有在因為未預期的伺服器問題而導致任務當掉時才需要這麼做。
Running Tasks on One Server
若要使用此功能,則專案必須使用 memcached
、redis
、dynamodb
、database
、file
、array
等其中一個快取 Driver 作為專案的預設快取 Driver。另外,所有的伺服器都必須要連線至相同的中央快取伺服器。
若專案的排程程式在多個伺服器上執行,則可限制排程任務只在單一伺服器上執行。舉例來說,假設我們設定了一個排程任務,每週五晚上會產生新報表。若任務排程程式在三個工作伺服器上執行,則這個排程任務會在這三台伺服器上都執行,且會產生三次報表。這可不好!
若要讓任務只在單一伺服器上執行,可在定義排程任務的時候使用 onOneServer
方法。第一個取得該任務的伺服器會先在該 Job 上確保一個 Atomic Lock,以防止其他伺服器在同一時間執行相同的任務:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('report:generate')4 ->fridays()5 ->at('17:00')6 ->onOneServer();
1use Illuminate\Support\Facades\Schedule;23Schedule::command('report:generate')4 ->fridays()5 ->at('17:00')6 ->onOneServer();
為單一伺服器 Job 命名
有時候,我們需要將排程分派相同的 Job,但使用不同的參數,且又要讓 Laravel 能在單一伺服器上以不同的兩種參數來執行這個 Job。這時,可以使用 name
方法來為各個排程定義指定一個不重複的名稱:
1Schedule::job(new CheckUptime('https://laravel.com'))2 ->name('check_uptime:laravel.com')3 ->everyFiveMinutes()4 ->onOneServer();56Schedule::job(new CheckUptime('https://vapor.laravel.com'))7 ->name('check_uptime:vapor.laravel.com')8 ->everyFiveMinutes()9 ->onOneServer();
1Schedule::job(new CheckUptime('https://laravel.com'))2 ->name('check_uptime:laravel.com')3 ->everyFiveMinutes()4 ->onOneServer();56Schedule::job(new CheckUptime('https://vapor.laravel.com'))7 ->name('check_uptime:vapor.laravel.com')8 ->everyFiveMinutes()9 ->onOneServer();
類似地,若要在單一伺服器上執行排程的閉包,也必須為這些閉包指定名稱:
1Schedule::call(fn () => User::resetApiRequestCount())2 ->name('reset-api-request-count')3 ->daily()4 ->onOneServer();
1Schedule::call(fn () => User::resetApiRequestCount())2 ->name('reset-api-request-count')3 ->daily()4 ->onOneServer();
背景任務
預設情況下,排程在同一個時間的多個任務會依照 schedule
方法中定義的順序依序執行。若任務的執行時間很長,則可能會導致接下來的任務比預期的時間還要完開始執行。若想讓任務在背景處理以同步執行多個任務,可使用 runInBackground
方法:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('analytics:report')4 ->daily()5 ->runInBackground();
1use Illuminate\Support\Facades\Schedule;23Schedule::command('analytics:report')4 ->daily()5 ->runInBackground();
runInBackground
方法只可用在 command
與 exec
方法所定義的排程任務上。
維護模式
若網站目前在維護模式下,則專案的排程任務可能不會執行,以避免任務影響伺服器上任何未完成的維護項目。不過,若仍想讓某個任務在維護模式中強制執行,可在定義任務時呼叫 eventInMaintenanceMode
方法:
1Schedule::command('emails:send')->evenInMaintenanceMode();
1Schedule::command('emails:send')->evenInMaintenanceMode();
Schedule Groups
When defining multiple scheduled tasks with similar configurations, you can use Laravel’s task grouping feature to avoid repeating the same settings for each task. Grouping tasks simplifies your code and ensures consistency across related tasks.
To create a group of scheduled tasks, invoke the desired task configuration methods, followed by the group
method. The group
method accepts a closure that is responsible for defining the tasks that share the specified configuration:
1use Illuminate\Support\Facades\Schedule;23Schedule::daily()4 ->onOneServer()5 ->timezone('America/New_York')6 ->group(function () {7 Schedule::command('emails:send --force');8 Schedule::command('emails:prune');9 });
1use Illuminate\Support\Facades\Schedule;23Schedule::daily()4 ->onOneServer()5 ->timezone('America/New_York')6 ->group(function () {7 Schedule::command('emails:send --force');8 Schedule::command('emails:prune');9 });
Running the Scheduler
現在,我們已經學會了如何定義排程任務,我們來看看要怎麼樣在伺服器上真正執行這些任務。schedule:run
Artisan 指令會取得所有的排程任務,並依照目前伺服器上的時間來判斷是否有需要執行這些任務。
因此,在使用 Laravel 的排程任務時,我們只需要在伺服器上新增單一一個 Cron 設定即可,該設定為每分鐘執行一次 schedule:run
指令。若讀者不知道如何在伺服器上新增 Cron 項目的話,可使用如 Laravel Forge 這樣的服務來協助你管理 Cron 設定:
1* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
1* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Sub-Minute Scheduled Tasks
On most operating systems, cron jobs are limited to running a maximum of once per minute. However, Laravel's scheduler allows you to schedule tasks to run at more frequent intervals, even as often as once per second:
1use Illuminate\Support\Facades\Schedule;23Schedule::call(function () {4 DB::table('recent_users')->delete();5})->everySecond();
1use Illuminate\Support\Facades\Schedule;23Schedule::call(function () {4 DB::table('recent_users')->delete();5})->everySecond();
When sub-minute tasks are defined within your application, the schedule:run
command will continue running until the end of the current minute instead of exiting immediately. This allows the command to invoke all required sub-minute tasks throughout the minute.
Since sub-minute tasks that take longer than expected to run could delay the execution of later sub-minute tasks, it is recommended that all sub-minute tasks dispatch queued jobs or background commands to handle the actual task processing:
1use App\Jobs\DeleteRecentUsers;23Schedule::job(new DeleteRecentUsers)->everyTenSeconds();45Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
1use App\Jobs\DeleteRecentUsers;23Schedule::job(new DeleteRecentUsers)->everyTenSeconds();45Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
Interrupting Sub-Minute Tasks
As the schedule:run
command runs for the entire minute of invocation when sub-minute tasks are defined, you may sometimes need to interrupt the command when deploying your application. Otherwise, an instance of the schedule:run
command that is already running would continue using your application's previously deployed code until the current minute ends.
To interrupt in-progress schedule:run
invocations, you may add the schedule:interrupt
command to your application's deployment script. This command should be invoked after your application is finished deploying:
1php artisan schedule:interrupt
1php artisan schedule:interrupt
Running the Scheduler Locally
一般來說,我們不會在本機開發機上新增排程程式的 Cron 設定。在本機上,不需要新增 Cron 設定,我們可以使用 schedule:work
Artisan 指令。該指令會在前景執行,並且會每分鐘叫用排程程式,直到手動停止該指令為止:
1php artisan schedule:work
1php artisan schedule:work
任務的輸出
Laravel 的排程程式提供了數種便利的方法可處理排程任務產生的輸出。首先,可使用 sendOutputTo
方法來將輸出傳送至檔案中以在稍後檢視:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->daily()5 ->sendOutputTo($filePath);
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->daily()5 ->sendOutputTo($filePath);
若想將輸出附加到給定檔案最後,可使用 appendOutputTo
方法:
1Schedule::command('emails:send')2 ->daily()3 ->appendOutputTo($filePath);
1Schedule::command('emails:send')2 ->daily()3 ->appendOutputTo($filePath);
若使用 emailOutputTo
方法,就可以將輸出以電子郵件傳送給指定的 E-Mail 位址。在將任務輸出以電子郵件寄出前,請先設定 Laravel 的電子郵件服務:
1Schedule::command('report:generate')2 ->daily()3 ->sendOutputTo($filePath)
1Schedule::command('report:generate')2 ->daily()3 ->sendOutputTo($filePath)
若只想在排程的 Artisan 或系統指令以結束代碼 0 以外的狀態退出時以電子郵件傳送輸出,可使用 emailOutputOnFailure
方法:
1Schedule::command('report:generate')2 ->daily()
1Schedule::command('report:generate')2 ->daily()
emailOutputTo
、emailOutputOnFailure
、sendOutputTo
、appendOutputTo
等方法只能在 command
與 exec
方法上使用。
任務的 Hook
使用 before
與 after
方法,即可指定要在排程任務執行前後執行的程式碼:
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->daily()5 ->before(function () {6 // The task is about to execute...7 })8 ->after(function () {9 // The task has executed...10 });
1use Illuminate\Support\Facades\Schedule;23Schedule::command('emails:send')4 ->daily()5 ->before(function () {6 // The task is about to execute...7 })8 ->after(function () {9 // The task has executed...10 });
使用 onSuccess
與 onFailure
方法,就可以指定要在排程任務成功或失敗時要執行的程式碼。「執行失敗」即為該排程的 Artisan 指令或系統指令以結束代碼 0 以外的代碼終止執行:
1Schedule::command('emails:send')2 ->daily()3 ->onSuccess(function () {4 // The task succeeded...5 })6 ->onFailure(function () {7 // The task failed...8 });
1Schedule::command('emails:send')2 ->daily()3 ->onSuccess(function () {4 // The task succeeded...5 })6 ->onFailure(function () {7 // The task failed...8 });
若指令有輸出,則可在 after
、onSuccess
、onFailure
等 Hook 上存取這些輸出。只需要在這些 Hook 的閉包定義上將 $output
引數型別提示為 Illuminate\Support\Stringable
即可:
1use Illuminate\Support\Stringable;23Schedule::command('emails:send')4 ->daily()5 ->onSuccess(function (Stringable $output) {6 // The task succeeded...7 })8 ->onFailure(function (Stringable $output) {9 // The task failed...10 });
1use Illuminate\Support\Stringable;23Schedule::command('emails:send')4 ->daily()5 ->onSuccess(function (Stringable $output) {6 // The task succeeded...7 })8 ->onFailure(function (Stringable $output) {9 // The task failed...10 });
Ping 網址
使用 pingBefore
與 thenPing
方法,就可讓排程程式在任務執行前後自動 Ping 給定的網址。該方法適合用來通知如 Envoyer 等的外部服務該排程任務已開始執行或已執行完畢:
1Schedule::command('emails:send')2 ->daily()3 ->pingBefore($url)4 ->thenPing($url);
1Schedule::command('emails:send')2 ->daily()3 ->pingBefore($url)4 ->thenPing($url);
pingBeforeIf
與 thenPingIf
方法可用來只在給定條件為 true
時 Ping 給定的網址:
1Schedule::command('emails:send')2 ->daily()3 ->pingBeforeIf($condition, $url)4 ->thenPingIf($condition, $url);
1Schedule::command('emails:send')2 ->daily()3 ->pingBeforeIf($condition, $url)4 ->thenPingIf($condition, $url);
pingOnSuccess
與 pingOnFailure
方法可用來只在任務執行成功或執行失敗時 Ping 給定的網址。「執行失敗」即為該排程的 Artisan 指令或系統指令以結束代碼 0 以外的代碼終止執行:
1Schedule::command('emails:send')2 ->daily()3 ->pingOnSuccess($successUrl)4 ->pingOnFailure($failureUrl);
1Schedule::command('emails:send')2 ->daily()3 ->pingOnSuccess($successUrl)4 ->pingOnFailure($failureUrl);
事件
Laravel dispatches a variety of events during the scheduling process. You may define listeners for any of the following events: