初始化本地仓库
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace core\scheduler;
|
||||
|
||||
class Client
|
||||
{
|
||||
|
||||
/**
|
||||
* 向redis 发送请求
|
||||
*
|
||||
* @param array $param
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function request(array $param): bool
|
||||
{
|
||||
$listen = 'tcp://' . config('core.scheduler.app.listen');
|
||||
try{
|
||||
$client = stream_socket_client($listen);
|
||||
fwrite($client, json_encode($param) . "\n"); // text协议末尾有个换行符"\n"
|
||||
$result = fgets($client);
|
||||
$arr = json_decode($result,true);
|
||||
if ($arr['code'] == 200){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}catch (\Throwable $e){
|
||||
throw new \Exception("请求任务server失败,请检查防火墙或者配置端口是否一致,{$listen}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace core\scheduler;
|
||||
|
||||
use app\service\admin\crontab\CrontabService;
|
||||
use support\Container;
|
||||
use Webman\RedisQueue\Client as RedisClient;
|
||||
use WebmanTech\SymfonyLock\Locker;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Crontab\Crontab;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* 该任务调度进程服务必须安装以下扩展组件
|
||||
* 1.定时任务 composer require workerman/crontab
|
||||
* 2.Redis composer require webman/redis-queue illuminate/redis
|
||||
* 3.业务锁 composer require webman-tech/symfony-lock
|
||||
* 4.env组件 composer require vlucas/phpdotenv
|
||||
* @method getAllTask() 获取所有任务
|
||||
* @method getTask($id) 获取单个任务
|
||||
* @method writeRunLog($insert_data = []) 写入运行日志
|
||||
* @method updateTaskRunState($id, $last_running_time) 更新任务状态
|
||||
*/
|
||||
class SchedulerServer
|
||||
{
|
||||
/**
|
||||
* 进程
|
||||
*
|
||||
* @var \Workerman\Worker
|
||||
*/
|
||||
protected \Workerman\Worker $worker;
|
||||
|
||||
/**
|
||||
* Redis
|
||||
* @var \Workerman\RedisQueue\Client|null
|
||||
*/
|
||||
protected \Workerman\RedisQueue\Client|null $redis;
|
||||
|
||||
/**
|
||||
* 调试模式
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $debug = false;
|
||||
/**
|
||||
* 是否运行定时任务呢
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $enabled = false;
|
||||
|
||||
/**
|
||||
* 日志
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $writeLog = true;
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* 任务进程池
|
||||
*
|
||||
* @var Crontab[] array
|
||||
*/
|
||||
private array $crontabPool = [];
|
||||
|
||||
/**
|
||||
* @var \Workerman\RedisQueue\Client|null 订阅实例
|
||||
*/
|
||||
protected \Workerman\RedisQueue\Client|null $subscribeClient;
|
||||
|
||||
/**
|
||||
* @var \Workerman\RedisQueue\Client|null 通知实例
|
||||
*/
|
||||
protected \Workerman\RedisQueue\Client|null $publishClient;
|
||||
|
||||
/**
|
||||
* @param Worker $worker
|
||||
*
|
||||
* @return void
|
||||
* 进程启动
|
||||
*/
|
||||
public function onWorkerStart(Worker $worker)
|
||||
{
|
||||
$config = config('core.scheduler.app');
|
||||
$this->debug = $config['debug'] ?? true;
|
||||
$this->writeLog = $config['write_log'] ?? true;
|
||||
$this->enabled = $config['enable'] ?? false;
|
||||
|
||||
if (!$this->enabled) {
|
||||
$this->writeln('定时任务未开启,如需开启,请修改配置 .env APP_TASK_ENABLED=true 或者core\\scheduler\\config\\app.php. enable = true ');
|
||||
return false;
|
||||
}
|
||||
$this->writeln("定时任务消息通道:{$config['listen']},请注意端口是否冲突,");
|
||||
$this->writeln("如果需要修改端口,请修改 core\\scheduler\\config\\app.php,process.php 两个文件");
|
||||
$this->config = $config;
|
||||
$this->worker = $worker;
|
||||
//订阅事件
|
||||
$this->subscribeEvent();
|
||||
//初始化任务
|
||||
$this->crontabInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
*/
|
||||
private function subscribeEvent(): void
|
||||
{
|
||||
|
||||
//创建发布实例
|
||||
if (empty($this->publishClient)) {
|
||||
$this->publishClient = $this->redisCreate();
|
||||
$this->writeln('启用心跳');
|
||||
Timer::add(30, function () {//保持链接活跃
|
||||
$this->writeln('发送心跳');
|
||||
$this->publishClient->send('ping', ['name' => 'ping', 'age' => 1]);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建订阅实例
|
||||
if (empty($this->subscribeClient)) {
|
||||
$this->subscribeClient = $this->redisCreate();
|
||||
}
|
||||
|
||||
//订阅一个心跳链接
|
||||
$this->subscribeClient->subscribe('ping', function ($data) {
|
||||
$this->writeln('收到心跳:' . json_encode($data, 256));
|
||||
});
|
||||
|
||||
//订阅任务改变事件
|
||||
$this->subscribeClient->subscribe('change_contrab', function ($data) {
|
||||
$args = $data['args'] ?? '';
|
||||
$this->crontabReload($args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 初始化
|
||||
*
|
||||
* @return \Webman\RedisQueue\Client|\Workerman\RedisQueue\Client|false
|
||||
*/
|
||||
function redisCreate(): RedisClient|\Workerman\RedisQueue\Client|false
|
||||
{
|
||||
$connection_name = 'default';
|
||||
try {
|
||||
$redis = RedisClient::connection($connection_name);
|
||||
} catch (\Exception $e) {
|
||||
var_dump($e->getMessage());
|
||||
$this->writeln('Redis初始化失败');
|
||||
return false;
|
||||
}
|
||||
return $redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TcpConnection $connection
|
||||
* @param $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function onMessage(TcpConnection $connection, $data): void
|
||||
{
|
||||
$data = json_decode($data, true);
|
||||
$method = $data['method'] ?? '';
|
||||
$args = $data['args'] ?? '';
|
||||
// 这里只有保留一个重启方法,对任务进行任何操作,直接调用重启解决
|
||||
if (!in_array($method, ['crontabReload'])) {
|
||||
$connection->send(json_encode(['code' => 400, 'msg' => "{$method} is not found"]));
|
||||
return;
|
||||
}
|
||||
$this->writeln('发送重启通知');
|
||||
//通知所有进程该任务进行重启
|
||||
$this->publishClient->send('change_contrab', ['method' => 'crontabReload', 'args' => $args]);
|
||||
$connection->send(json_encode(['code' => 200, 'msg' => "ok"]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启定时任务
|
||||
*
|
||||
* @param array $param
|
||||
*/
|
||||
private function crontabReload(array $param): void
|
||||
{
|
||||
$ids = explode(',', (string)($param['id'] ?? ''));
|
||||
foreach ($ids as $id) {
|
||||
if (isset($this->crontabPool[$id])) {
|
||||
$this->writeln('销毁定时器,ID:' . $id);
|
||||
$this->crontabPool[$id]['crontab']->destroy();
|
||||
unset($this->crontabPool[$id]);
|
||||
}
|
||||
$this->crontabRun($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化定时任务
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function crontabInit(): void
|
||||
{
|
||||
/**
|
||||
* 定时任务服务类
|
||||
*/
|
||||
$systemCrontabService = Container::make(CrontabService::class);
|
||||
$data = $systemCrontabService->getTaskAll('id');
|
||||
$this->writeln('定时器任务数:' . count($data));
|
||||
if (!empty($data)) {
|
||||
foreach ($data as $item) {
|
||||
$this->crontabRun($item['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建定时器
|
||||
*
|
||||
* @param string|int $id
|
||||
*/
|
||||
private function crontabRun(string|int $id): void
|
||||
{
|
||||
$systemCrontabService = Container::make(CrontabService::class);
|
||||
$data = $systemCrontabService->getTask($id);
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
if (intval($data['enabled']) === 0) {
|
||||
return;
|
||||
}
|
||||
$crontab = new Crontab($data['rule'], callback: function () use (&$data, $systemCrontabService) { //这里传参必须传引用
|
||||
//首次不执行(如果数据库中标记为未启动)
|
||||
if (empty($data['first_start'])) {
|
||||
$data['first_start'] = 1;
|
||||
// 持久化到数据库(直接使用DAO层)
|
||||
$systemCrontabService->dao->update($data['id'], ['first_start' => 1]);
|
||||
$this->writeln("任务#{$data['id']} 首次执行跳过,已更新 first_start 状态");
|
||||
return false;
|
||||
}
|
||||
//运行次数加1,很重要,多进程情况下用来检测当前次数是否已执行
|
||||
$data['running_times'] += 1;
|
||||
$uuid = $this->createTaskUuid($data);
|
||||
//获取锁.
|
||||
$lock = Locker::cash($uuid);
|
||||
// 获取锁失败
|
||||
if (!$lock) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
/** @var CrontabService $systemCrontabService */
|
||||
$systemCrontabService = Container::make(CrontabService::class);
|
||||
$reData = $systemCrontabService->runOneTask($data['id']);
|
||||
$this->writeln( $data['title'].' 任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $reData['code']);
|
||||
$this->isSingleton($data);
|
||||
} finally {
|
||||
// 释放锁
|
||||
$lock->release();
|
||||
}
|
||||
});
|
||||
|
||||
$this->crontabPool[$data['id']] = [
|
||||
'id' => $data['id'],
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
'crontab' => $crontab,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否单次
|
||||
*
|
||||
* @param $crontab
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function isSingleton($crontab): void
|
||||
{
|
||||
if ($crontab['singleton'] == 0 && isset($this->crontabPool[$crontab['id']])) {
|
||||
$this->writeln("定时器销毁", true);
|
||||
$this->crontabPool[$crontab['id']]['crontab']->destroy();
|
||||
unset($this->crontabPool[$crontab['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $crontab
|
||||
*
|
||||
* @return string
|
||||
* 创建任务uuid
|
||||
*/
|
||||
private function createTaskUuid($crontab): string
|
||||
{
|
||||
// 以任务id + 运行次数作为唯一id, 这样在过期范围内,只会有一个进程能执行到该次任务
|
||||
return 'crontab_' . $crontab['id'] . '_' . $crontab['running_times'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出日志
|
||||
*
|
||||
* @param $msg
|
||||
* @param bool $isSuccess
|
||||
*/
|
||||
private function writeln($msg, bool $isSuccess = false): void
|
||||
{
|
||||
if ($this->debug) {
|
||||
$workerId = isset($this->worker) && !empty($this->worker) ? $this->worker->id : 0;
|
||||
echo 'webman-scheduler:' . $workerId . ' [' . date('Y-m-d H:i:s') . '] ' . $msg . ($isSuccess == 0 ? " [Ok] " : " [Fail] ") . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
use core\scheduler\event\EvalTask;
|
||||
use core\scheduler\event\SchedulingTask;
|
||||
use core\scheduler\event\ShellTask;
|
||||
use core\scheduler\event\UrlTask;
|
||||
|
||||
return [
|
||||
'enable' => env('APP_TASK_ENABLED', false),// 是否启用定时器 修改此参数后,需要重启
|
||||
'debug' => config('app.debug'),
|
||||
'write_log' => false,
|
||||
'listen' => '127.0.0.1:' . '2001',// 注意此端口用于任务通讯,一个项目一个端口,请勿占用
|
||||
'task_handle' => [
|
||||
//任务操作类
|
||||
\app\enum\system\TaskScheduleType::TASK_URL->value => UrlTask::class,
|
||||
\app\enum\system\TaskScheduleType::TASK_EVAL->value => EvalTask::class,
|
||||
\app\enum\system\TaskScheduleType::TASK_SHELL->value => ShellTask::class,
|
||||
\app\enum\system\TaskScheduleType::TASK_SCHEDULE->value => SchedulingTask::class,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
namespace core\scheduler\event;
|
||||
|
||||
|
||||
use app\enum\system\OperationResult;
|
||||
use core\scheduler\event\EventBootstrap;
|
||||
|
||||
class EvalTask implements EventBootstrap
|
||||
{
|
||||
/**
|
||||
* @param $crontab
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parse($crontab): array
|
||||
{
|
||||
$code = OperationResult::SUCCESS->value;
|
||||
try {
|
||||
$log = eval($crontab['target']);
|
||||
} catch (\Throwable $throwable) {
|
||||
$code = OperationResult::FAILURE->value;
|
||||
$log = $throwable->getMessage();
|
||||
}
|
||||
return ['log'=> $log, 'code' => $code];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
namespace core\scheduler\event;
|
||||
|
||||
/**
|
||||
* 解析任务
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
interface EventBootstrap
|
||||
{
|
||||
public static function parse($crontab): mixed;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace core\scheduler\event;
|
||||
|
||||
use app\enum\system\OperationResult;
|
||||
use core\scheduler\event\EventBootstrap;
|
||||
|
||||
class SchedulingTask implements EventBootstrap
|
||||
{
|
||||
/**
|
||||
* @param $crontab
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parse($crontab): array
|
||||
{
|
||||
$code = OperationResult::SUCCESS->value;
|
||||
try {
|
||||
$className = $crontab['target'];
|
||||
$bizId = $crontab['biz_id'];
|
||||
// 创建类的实例
|
||||
$instance = new $className();
|
||||
// 使用反射动态调用非静态方法
|
||||
$reflectionMethod = new \ReflectionMethod($className, 'run');
|
||||
$log = $reflectionMethod->invokeArgs($instance, [$bizId]);
|
||||
} catch (\Throwable $throwable) {
|
||||
$code = OperationResult::FAILURE->value;
|
||||
$log = $throwable->getMessage();
|
||||
}
|
||||
return ['log' => $log, 'code' => $code];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace core\scheduler\event;
|
||||
|
||||
use app\enum\system\OperationResult;
|
||||
use core\scheduler\event\EventBootstrap;
|
||||
|
||||
class ShellTask implements EventBootstrap
|
||||
{
|
||||
/**
|
||||
* @param $crontab
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parse($crontab): array
|
||||
{
|
||||
$code = OperationResult::SUCCESS->value;
|
||||
try {
|
||||
$log = shell_exec($crontab['target']);
|
||||
} catch (\Throwable $e) {
|
||||
$code = OperationResult::FAILURE->value;
|
||||
$log = $e->getMessage();
|
||||
}
|
||||
return ['code' => $code, 'log' => $log];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace core\scheduler\event;
|
||||
|
||||
use app\enum\system\OperationResult;
|
||||
use core\scheduler\event\EventBootstrap;
|
||||
|
||||
class UrlTask implements EventBootstrap
|
||||
{
|
||||
/**
|
||||
* @param $crontab
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parse($crontab): array
|
||||
{
|
||||
$url = trim($crontab['target'] ?? '');
|
||||
$code = OperationResult::SUCCESS->value;
|
||||
try {
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->get($url);
|
||||
$log = strip_tags($response->getBody()->getContents());
|
||||
} catch (\Throwable $throwable) {
|
||||
$code = OperationResult::FAILURE->value;
|
||||
$log = $throwable->getMessage();
|
||||
}
|
||||
return ['code' => $code, 'log' => $log];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user