教你更優雅地寫 API 之「靈活地任務排程」

Jianne發表於2021-06-23

任務排程

前言

Laravel 中可以很方便地定義任務排程,在 app/Console/Kernel.phpschedule 方法中定義 callcommandjob 以及exec 的執行間隔即可。

在實際開發過程中,我們發現如果需要修改任務排程的執行時間間隔,或者關閉某個任務排程,都需要重新修改程式碼提交,重新構建釋出,體驗不是很好。

這裡分享一個基於資料表的配置來管理 Laravel 應用程式中任務排程的方案,可以一起參與討論一下。

實現過程

在討論實現之前,先梳理一下需要優化的點,並整理一下實現思路。

需求

  • 能夠靈活地配置任務排程的執行間隔
  • 允許開啟關閉任務的排程
  • 適配 laravel 的任務排程引數,保持風格統一
  • 簡單地封裝擴充套件,不增加負擔

思路

可以在 Schedule 例項化以後通過讀取 schedules 資料表的配置來定義執行任務排程,可以在此基礎上進行簡單封裝讓多個專案中也可以使用。

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedules = ScheduleModel::active()->get();
    foreach($schedules as $schedule){
        $schedule->command($schedule->command .'' .$schedule->parameters)->cron($schedule->expression);
    }

    // $schedule->command('inspire')->hourly();
}

實現

Schedule 通過服務容器 singleton 例項化後依賴注入,可以通過容器的 resolving 方法繫結一個回撥函式在 Schedule 例項化後執行,在回撥函式中加入讀取 schedules 配置的邏輯。

// vendor/jiannei/laravel-schedule/src/Providers/LaravelServiceProvider.php

$this->app->resolving(Schedule::class, function ($schedule) {
    $this->schedule($schedule);
});

protected function schedule(Schedule $schedule): void
{
    try {
        $schedules = app(Config::get('schedule.model'))->active()->get();
    } catch (QueryException $exception) {
        $schedules = collect();
    }

    $schedules->each(function ($item) use ($schedule) {
        $event = $schedule->command($item->command.' '.$item->parameters);

        $event->cron($item->expression)
            ->name($item->description)
            ->timezone($item->timezone);

        if (class_exists($enum = Config::get('schedule.enum'))) {
            $scheduleEnum = $enum::fromValue($item->command);
            $callbacks = ['skip', 'when', 'before', 'after', 'onSuccess', 'onFailure'];
            foreach ($callbacks as $callback) {
                if ($method = $scheduleEnum->hasCallback($callback)) {
                    $event->$callback($scheduleEnum->$method($event, $item));
                }
            }
        }

        if ($item->environments) {
            $event->environments($item->environments);
        }

        if ($item->without_overlapping) {
            $event->withoutOverlapping($item->without_overlapping);
        }

        if ($item->on_one_server) {
            $event->onOneServer();
        }

        if ($item->in_background) {
            $event->runInBackground();
        }

        if ($item->in_maintenance_mode) {
            $event->evenInMaintenanceMode();
        }

        if ($item->output_file_path) {
            if ($item->output_append) {
                $event->appendOutputTo(Config::get('schedule.output.path').Str::start($item->output_file_path, DIRECTORY_SEPARATOR));
            } else {
                $event->sendOutputTo(Config::get('schedule.output.path').Str::start($item->output_file_path, DIRECTORY_SEPARATOR));
            }
        }

        if ($item->output_email) {
            if ($item->output_email_on_failure) {
                $event->emailOutputOnFailure($item->output_email);
            } else {
                $event->emailOutputTo($item->output_email);
            }
        }
    });
}

安裝和使用

Package 已釋出,可以檢視相應的文件

Github文件
Gitee文件

原理

在實現前面的需求後,一起討論下 Laravel 應用中通過 php artisan schedule:run 能夠進行任務排程的原理。

在 Laravel 專案中部署任務排程,通常的 Linux crontab 配置如下:

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

這裡涉及到使用 Linux 的 crontab 每分鐘通過 php-cli 間隔執行 Laravel 的 artisan 檔案

php artisan schedule:run

說明:

  • php cli 模式下每分鐘間隔執行 Laravel 的 artisan 檔案
  • artisan 是 Laravel 命令列執行模式的入口檔案
  • 通過 artisan 入口檔案,解析後面的 schedule:run 引數,最終執行 vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php 中的 handle 方法

1. php artisan 的執行

  • bootstrap/app.php
// 註冊 Illuminate\Contracts\Console\Kernel::class和App\Console\Kernel::class 的繫結關係
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);
  • artisan
// 根據上一步的繫結關係,例項化 App\Console\Kernel
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

// 執行 App\Console\Kernel 的 handle 方法
$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput,
    new Symfony\Component\Console\Output\ConsoleOutput
);

// 執行 App\Console\Kernel 的 terminate 方法
$kernel->terminate($input, $status);

exit($status);
  • app/Console/Kernel.php 中繼承了 Illuminate\Foundation\Console\Kernelhandleterminate 方法
  • vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php 我們需要關心 __constructhandleterminate
public function __construct(Application $app, Dispatcher $events)
{
    if (! defined('ARTISAN_BINARY')) {
        define('ARTISAN_BINARY', 'artisan');
    }

    $this->app = $app;
    $this->events = $events;

        // 在應用服務啟動後執行控制檯執任務
    $this->app->booted(function () {
        $this->defineConsoleSchedule();
    });
}

public function handle($input, $output = null)
{
    try {
        $this->bootstrap();

        return $this->getArtisan()->run($input, $output);
    } catch (Throwable $e) {
        $this->reportException($e);

        $this->renderException($output, $e);

        return 1;
    }
}

public function terminate($input, $status)
{
    $this->app->terminate();
}

protected function defineConsoleSchedule()
{
        //  Illuminate\Console\Scheduling\Schedule::class 例項化時呼叫 schedule 方法執行任務排程
    $this->app->singleton(Schedule::class, function ($app) {
        return tap(new Schedule($this->scheduleTimezone()), function ($schedule) {
            $this->schedule($schedule->useCache($this->scheduleCache()));
        });
    });
}
  • app/Console/Kernel.php 中覆蓋了 Illuminate\Foundation\Console\Kernelschedule 方法,也就是以前經常定義任務排程執行的地方
protected function schedule(Schedule $schedule)
{
    // $schedule->command('inspire')->hourly();
}

從上面的分析可以看出,php artisan 執行會註冊Illuminate\Console\Scheduling\Schedule::class ,等Illuminate\Console\Scheduling\Schedule::class 例項化時執行定義在 app/Console/Kernel.phpschedule 方法中定義的任務排程。

補充:

  • php artisan 等價於 php artisan list ,
  • 分析 vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php 中的 getArtisan 方法可以瞭解如何將 artisan 後面的 list 引數解析成需要執行的 command

2. php artisan schedule:run 的執行

  • artisan 解析schedule:run 引數,執行 vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php 中的 handle 方法

  • handle 方法中注入 \Illuminate\Console\Scheduling\Schedule 例項

// vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php

public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
    $this->schedule = $schedule;
    $this->dispatcher = $dispatcher;
    $this->handler = $handler;

    foreach ($this->schedule->dueEvents($this->laravel) as $event) {
        if (! $event->filtersPass($this->laravel)) {
            $this->dispatcher->dispatch(new ScheduledTaskSkipped($event));

            continue;
        }

        if ($event->onOneServer) {
            $this->runSingleServerEvent($event);
        } else {
            $this->runEvent($event);
        }

        $this->eventsRan = true;
    }

    if (! $this->eventsRan) {
        $this->info('No scheduled commands are ready to run.');
    }
}
  • 結合前面 php artisan 的分析,在 \Illuminate\Console\Scheduling\Schedule 例項化時便會呼叫 app/Console/Kernel.php 中的 schedule方法中定義的任務排程

其他

如果對您的日常工作有所幫助或啟發,歡迎 star + fork + follow

如果有任何批評建議,通過郵箱(longjian.huang@foxmail.com)的方式可以聯絡到我。

總之,歡迎各路英雄好漢。

QQ 群:1105120693

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章