Laravel: Task Scheduling

Trong quá trình xây dựng web, đôi khi có những công việc chúng ta cần nên kết hoạch cho nó chạy vào một khoảng thời gian nhất định trong ngày, trong tuần hay trong tháng ... Trước đây, với những phiên bản Laravel cũ, bạn cần phải định nghĩa nhiều con Cron chạy trên hệ điều hành mỗi con cron sẽ đảm nhiệm 1 chức năng là thực hiện một schedule mà bạn muốn hệ thống chạy. Bây giờ, công việc này đã được cải thiện hơn nhiều, với việc sử dụng schedule của Laravel, thay vì tạo nhiều lệnh cron thì bạn chỉ cần chạy một cron duy nhất là :

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

Command này sẽ gọi Laravel schedule mỗi phút một lần. Khi câu lệnh schedule:run được thực thi, Laravel sẽ thực hiện các schedule mà bạn đã định nghĩa trước đó.

Định nghĩa Schedule

Bạn có thể định nghĩa tất cả các shedule task của mình trong function schedule của file App\Console\Kernel. Hãy theo dõi ví dụ sau:

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {

        $schedule->command('inspire')
            ->everyFiveMinutes()
            ->appendOutputTo($filePath);
    }

    /**
     * Register the Closure based commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        require base_path('routes/console.php');
    }
}

Ở ví dụ trên, hệ thóng sẽ thự thi câu lệnh php artisan inspire cứ 5 phút một lần và ghi nó vào file với đường dẫn là $filePath. Câu lệnh php artisan inspire là câu lệnh hiển thị ngẫu nhiên một câu danh ngôn.

Phạm vi làm việc

Khả năng của Laravel Task Schedule rất phong phú, nó cho phép bạn lập thời gian biểu để chạy các ứng dụng sau:

  • Gọi đến Closure:
$schedule->call(function () {
    DB::table('temp_data')->delete();
})->daily();
  • Thực thi một câu lệnh Artisan
$schedule->command('emails:send --force')->daily();

$schedule->command(EmailsCommand::class, ['--force'])->daily();
  • Scheduling Queued Jobs
$schedule->job(new Heartbeat)->everyFiveMinutes();
  • Thực hiện một câu lệnh trong hệ điều hành:
$schedule->exec('ipconfig -all')->daily();

Đặt tần suất thực hiện

Một chú ý là tại sao trong phần đặt lịch của Hệ điều hành chúng ta đã thực hiện đặt lịch nhưng trong Task Scheduling của Laravel chúng ta lại đặt lịch tiếp? Cái nào ưu việt hơn? Nếu bạn muốn đặt lịch trong Laravel thì lịch của hệ điều hành nên để là thực hiện mỗi phút một lần (trong cron job để là * * * * *) và lịch chi tiết sẽ thực hiện trong Laravel. Mỗi cách đặt lịch có một số ưu điểm:

  • Đặt lịch chính xác thông qua hệ điều hành sẽ tối ưu hóa về thực hiện, ví dụ chúng ta có lịch thực hiện 1 tuần một lần, nếu đặt lịch hệ thống là mỗi phút một lần như vậy sẽ có nhiều trigger bung ra để thực hiện lệnh trong Laravel.
  • Đặt lịch hệ thống mỗi phút một lần và lịch chính xác trong Laravel giúp chúng ta có thể thiết lập lịch trong ứng dụng, có thể lưu trữ lịch thực hiện trong database.
Phương Thức Mô tả
->cron('* * * * * *'); Đặt lịch kiểu cron job
->everyMinute(); Thực hiện công việc mỗi phút một lần
->everyFiveMinutes(); Thực hiện công việc 5 phút một lần
->everyTenMinutes(); Thực hiện công việc 10 phút một lần
->everyThirtyMinutes(); Thực hiện công việc 30 phút một lần
->hourly(); Thực hiện công việc mỗi giờ một lần
->hourlyAt(15); Thực hiện công việc mỗi giờ một lần vào phút thứ 15
->daily(); Thực hiện công việc hàng ngày vào đúng nửa đêm
->dailyAt('13:00'); Thực hiện công việc hàng ngày vào lúc 13:00
->twiceDaily(1, 13); Thực hiện công việc 2 lần một ngày vào lúc 1:00 và 13:00
->weekly(); Thực hiện công việc hàng tuần
->monthly(); Thực hiện công việc hàng tháng
->monthlyOn(4, '15:00'); Thực hiện công việc vào 15:00 ngày thứ 4 của tháng hàng tháng
->quarterly(); Thực hiện công việc hàng quý
->yearly(); Thực hiện công việc hàng năm

Chạy Schedule với điều kiện

Phương Thức Mô tả
->weekdays(); Giới hạn thực hiện lịch chỉ vào ngày cuối tuần
->sundays(); Giới hạn thực hiện lịch chỉ vào Chủ nhật
->mondays(); Giới hạn thực hiện lịch chỉ vào thứ Hai
->tuesdays(); Giới hạn thực hiện lịch chỉ vào thứ Ba
->wednesdays(); Giới hạn thực hiện lịch chỉ vào thứ Tư
->thursdays(); Giới hạn thực hiện lịch chỉ vào thứ Năm
->fridays(); Giới hạn thực hiện lịch chỉ vào thứ Sáu
->saturdays(); Giới hạn thực hiện lịch chỉ vào thứ Bảy
->between($start, $end); Giới hạn thực hiện lịch ở giữa khoảng thời gian từ $start đến $end
->when(Closure); Giới hạn thực hiện lịch chỉ khi nào Closure trả về true.

Laravel cho phép bạn thực hiện các điều kiện phức tạp trong thực hiện lịch công việc với việc sử dụng phương thức when và skip bằng cách truyền vào một Closure. Với phương thức when() nếu Closure trả về true thì lịch sẽ được thực hiện, phương thức skip() thì ngược lại, nếu Closure trả về true thì lịch sẽ được bỏ qua. Ví dụ:

$schedule->call(function () {
        DB::table('temp_users')->delete();
    })->daily()
    ->when(function () {
        return (DB::table('loging_user')->get() != null)?false:true;
    });

Ngăn chặn thực hiện lệnh chồng chéo

Khi thực hiện chạy task schedule, không thể không tránh khỏi tình trạng một câu lệnh trước đang thực hiện(chưa thực hiện xong) mà một câu lệnh khác đã được gọi. Với phương thức withoutOverlapping Laravel sẽ bỏ qua câu lệnh sau nếu như câu lệnh trước đó chưa hoàn thành.

$schedule->command('report:summary')->withoutOverlapping();

Laravel sử dụng khái niệm mutex trong lập trình để giải quyết vấn dề xử lý đa luồng. Mutex sử dung một cờ chung, hoạt đồng như người gác cổng, cho phép 1 luồng có thể vào và chặn các luồng khác cho đến khi luồng vào kết thúc quá trình thực hiện. Laravel tạo ra một mutex khi bắt đầu thực hiện một công việc và mỗi lần thực thi nó sẽ kiểm tra nếu mutex tồn tại, công việc đó sẽ không được thực hiện. Bạn có thể xem cách mutex hoạt động trong phương thức withoutOverlapping trong class Illuminate/Console/Scheduling/Event.php:

public function withoutOverlapping()
    {
        $this->withoutOverlapping = true;

        return $this->then(function () {
            $this->mutex->forget($this);
        })->skip(function () {
            return $this->mutex->exists($this);
        });
    }

Laravel tạo ra một phương thức callback để lọc và thông báo với Schedule Manager bỏ qua công việc nếu một mutex vẫn đang tồn tại, nó cũng tạo ra một callback để xóa mutex sau khi công việc hoàn thành. Trước khi thực hiện công việc, Laravel thực hiện kiểm tra trong phương thức run của Illuminate/Console/Scheduling/Event:

    public function run(Container $container)
    {
        if ($this->withoutOverlapping &&
            ! $this->mutex->create($this)) {
            return;
        }

        $this->runInBackground
                    ? $this->runCommandInBackground($container)
                    : $this->runCommandInForeground($container);
    }

Ghi dữ liệu ra ngoài khi chạy schedule

Laravel cung cấp rất nhiều phương thức hỗ trợ xuất dữ liệu đầu ra khi thực hiện các công việc:

  • sendOutputTo: xuất dữ liệu ra một file trong một thư mục:
$schedule->exec('ipconfig')
         ->daily()
         ->sendOutputTo($filePath);
  • appendOutputTo: Ghi tiếp vào file đã tồn tại:
$schedule->exec('ipconfig')
         ->daily()
         ->appendOutputTo($filePath);

Task Hook

Laravel cho phép bạn có thể thực hiện một số các công việc trước hoặc sau khi một lịch thực hiện công việc được bắt đầu hoặc kết thúc thông qua các phương thức before(), after():

$schedule->command('emails:send')
         ->daily()
         ->before(function () {
             // Task is about to start...
         })
         ->after(function () {
             // Task is complete...
         });

Ngoài ra Laravel có các phương thức pingBefore, thenPing để có thể thực hiện ping một URL một cách tự động trước hoặc sau khi một nhiệm vụ được hoàn thành.

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