教你更优雅地写 API 之「灵活地任务调度」

任务调度

前言

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 $item){
        $schedule->command($item->command .'' .$item->parameters)->cron($item->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 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
Image
Jianne
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

感觉好牛逼的样子,哈哈!

4年前 评论
Jianne

@dongzhiyu 我记得你这个 ID

4年前 评论
Image dongzhiyu 4年前
Image dongzhiyu 4年前
Image Jianne (作者) (楼主) 4年前
Image dongzhiyu 4年前

执行artisan命令就报错, 是不是集成的Enum有啥变动 file

4年前 评论
Image Jianne (楼主) 4年前
Image 大表哥 (作者) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!