diff --git a/composer.json b/composer.json index 81fc6a58..528bc509 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "ext-json": "*", "ext-posix": "*", "doctrine/dbal": "^2.13.1", + "dragonmantank/cron-expression": "^3.3", "jelix/version": "^2.0", "koriym/attributes": "^1.0", "psy/psysh": "^0.11.2", diff --git a/docs/guide/errcode.md b/docs/guide/errcode.md index 7aa819b5..cf3adff8 100644 --- a/docs/guide/errcode.md +++ b/docs/guide/errcode.md @@ -76,4 +76,6 @@ | E00072 | 上下文无法找到 | 检查上下文环境,如是否处于协程环境中 | | E00073 | 在类中找不到方法 | 检查调用对象是否存在对应的方法(method)或检查是否插入了对应的macro宏方法 | | E00074 | 参数非法 | 检查调用的参数是否正常(此处可能有多处问题,请看具体调用栈炸掉的地方) | +| E00075 | Cron表达式非法 | 检查 @Cron 中的表达式是否书写格式正确 | +| E00076 | Cron检查间隔非法 | 检查 @Cron 中的检查间隔(毫秒)是否在1000-60000之间 | | E99999 | 未知错误 | | diff --git a/docs/update/build-update.md b/docs/update/build-update.md index 34f217eb..1c9d02e3 100644 --- a/docs/update/build-update.md +++ b/docs/update/build-update.md @@ -4,6 +4,23 @@ 同时此处将只使用 build 版本号进行区分。 +## build 453 (2022-3-25) + +- 新增 property 注解用于 IDE 识别 + +## build 452 (2022-3-25) + +- 修复 OnSetup 注解无法使用 Attribute 解析的 Bug +- 修复 HelpGenerator 的 Alias 不工作的 Bug + +## build 451 (2022-3-21) + +- 重构全局函数,统一函数命名,并补全注释 + +## build 450 (2022-3-21) + +- 新增命令帮助生成器 + ## build 449 (2022-3-21) - 新增 Composer 模块加载和分发模式 diff --git a/docs/update/v2.md b/docs/update/v2.md index 52d1d52a..74065a8d 100644 --- a/docs/update/v2.md +++ b/docs/update/v2.md @@ -1,5 +1,15 @@ # 更新日志(v2 版本) +## v2.7.3(build 453) + +> 更新时间:2022.3.25 + +- 新增命令帮助生成器 +- 重构全局函数,统一函数命名,并补全注释 +- 修复 OnSetup 注解无法使用 Attribute 解析的 Bug +- 修复 HelpGenerator 的 Alias 不工作的 Bug +- 新增 property 注解用于 IDE 识别 + ## v2.7.2(build 449) > 更新时间:2022.3.21 diff --git a/src/ZM/Annotation/Cron/Cron.php b/src/ZM/Annotation/Cron/Cron.php new file mode 100644 index 00000000..4db8294a --- /dev/null +++ b/src/ZM/Annotation/Cron/Cron.php @@ -0,0 +1,85 @@ +expression = $expression; + $this->worker_id = $worker_id; + $this->check_delay_time = $check_delay_time; + $this->max_iteration_count = $max_iteration_count; + } + + public function getStatus(): int + { + return $this->status; + } + + /** + * @internal + */ + public function setStatus(int $status): void + { + $this->status = $status; + } + + /** + * @internal + */ + public function getRecordNextTime(): int + { + return $this->record_next_time; + } + + /** + * @internal + */ + public function setRecordNextTime(int $record_next_time): void + { + $this->record_next_time = $record_next_time; + } +} diff --git a/src/ZM/Command/Server/ServerStopCommand.php b/src/ZM/Command/Server/ServerStopCommand.php index 233fe6de..165faad8 100644 --- a/src/ZM/Command/Server/ServerStopCommand.php +++ b/src/ZM/Command/Server/ServerStopCommand.php @@ -33,7 +33,7 @@ class ServerStopCommand extends DaemonCommand $name = explode('.', $file); if (end($name) == 'pid') { $pid = file_get_contents($file_path . '/' . $file); - Process::kill($pid, SIGKILL); + Process::kill((int) $pid, SIGKILL); } elseif ($file === 'master.json') { $json = json_decode(file_get_contents($file_path . '/' . $file), true); Process::kill($json['pid'], SIGKILL); diff --git a/src/ZM/Event/SwooleEvent/OnMessage.php b/src/ZM/Event/SwooleEvent/OnMessage.php index d1ff6538..56d803bc 100644 --- a/src/ZM/Event/SwooleEvent/OnMessage.php +++ b/src/ZM/Event/SwooleEvent/OnMessage.php @@ -31,11 +31,8 @@ class OnMessage implements SwooleEvent $conn = ManagerGM::get($frame->fd); set_coroutine_params(['server' => $server, 'frame' => $frame, 'connection' => $conn]); $dispatcher1 = new EventDispatcher(OnMessageEvent::class); - $dispatcher1->setRuleFunction(function ($v) { - if ($v->getRule() == '') { - return true; - } - return eval('return ' . $v->getRule() . ';'); + $dispatcher1->setRuleFunction(function ($v) use ($conn) { + return $v->connect_type === $conn->getName() && ($v->getRule() === '' || eval('return ' . $v->getRule() . ';')); }); $dispatcher = new EventDispatcher(OnSwooleEvent::class); diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStart.php b/src/ZM/Event/SwooleEvent/OnWorkerStart.php index b27730d9..1f8dd59e 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStart.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStart.php @@ -35,6 +35,7 @@ use ZM\Store\LightCacheInside; use ZM\Store\MySQL\SqlPoolStorage; use ZM\Store\Redis\ZMRedisPool; use ZM\Utils\DataProvider; +use ZM\Utils\Manager\CronManager; use ZM\Utils\Manager\ModuleManager; use ZM\Utils\SignalListener; @@ -94,8 +95,9 @@ class OnWorkerStart implements SwooleEvent } $this->loadAnnotations(); // 加载composer资源、phar外置包、注解解析注册等 - + CronManager::initCronTasks(); // 初始化定时任务 EventManager::registerTimerTick(); // 启动计时器 + set_coroutine_params(['server' => $server, 'worker_id' => $worker_id]); $dispatcher = new EventDispatcher(OnStart::class); $dispatcher->setRuleFunction(function ($v) { diff --git a/src/ZM/Utils/Manager/CronManager.php b/src/ZM/Utils/Manager/CronManager.php new file mode 100644 index 00000000..03400fd6 --- /dev/null +++ b/src/ZM/Utils/Manager/CronManager.php @@ -0,0 +1,110 @@ +worker_id !== $v->worker_id && $v->worker_id != -1) { + return; + } + try { + if (strpos($v->expression, '\\') !== 0) { + $v->expression = str_replace('\\', '/', $v->expression); + } + $cron = new CronExpression($v->expression); + $cron->setMaxIterationCount($v->max_iteration_count); + $plain_class = $v->class; + Console::debug("Cron task checker starting {$plain_class}:{$v->method}, next run at {$cron->getNextRunDate()->format('Y-m-d H:i:s')}"); + if ($v->check_delay_time > 60000 || $v->check_delay_time < 1000) { + Console::warning(zm_internal_errcode('E00076') . 'Delay time must be between 1000 and 60000, reset to 20000'); + $v->check_delay_time = 20000; + } + } catch (InvalidArgumentException $e) { + Console::error(zm_internal_errcode('E00075') . 'Invalid cron expression or arguments, please check it!'); + throw $e; + } + + Timer::tick($v->check_delay_time, static function () use ($v, $dispatcher, $cron) { + set_coroutine_params([]); + if (ZMAtomic::get('stop_signal')->get() != 0) { + Timer::clearAll(); + return; + } + try { + Console::debug('Cron: ' . ($cron->isDue() ? 'true' : 'false') . ', last: ' . $cron->getPreviousRunDate()->format('Y-m-d H:i:s') . ', next: ' . $cron->getNextRunDate()->format('Y-m-d H:i:s')); + if ($cron->isDue()) { + if ($v->getStatus() === 0) { + self::startExecute($v, $dispatcher, $cron); + } elseif ($v->getStatus() === 2) { + if ($v->getRecordNextTime() !== $cron->getNextRunDate()->getTimestamp()) { + self::startExecute($v, $dispatcher, $cron); + } + } + } else { + if ($v->getStatus() === 2 && $v->getRecordNextTime()) { + $v->setStatus(0); + } + } + } catch (Exception $e) { + Console::error(zm_internal_errcode('E00034') . 'Uncaught error from Cron: ' . $e->getMessage() . ' at ' . $e->getFile() . "({$e->getLine()})"); + } catch (Error $e) { + Console::error(zm_internal_errcode('E00034') . 'Uncaught fatal error from Cron: ' . $e->getMessage()); + echo Console::setColor($e->getTraceAsString(), 'gray'); + Console::error('Please check your code!'); + } + }); + } + } + + /** + * @throws InterruptException + * @throws AnnotationException + * @throws Exception + */ + private static function startExecute(Cron $v, EventDispatcher $dispatcher, CronExpression $cron) + { + Console::verbose("Cron task {$v->class}:{$v->method} is due, running at " . date('Y-m-d H:i:s') . ($v->getRecordNextTime() === 0 ? '' : (', offset ' . (time() - $v->getRecordNextTime()) . 's'))); + $v->setStatus(1); + $starttime = microtime(true); + $pre_next_time = $cron->getNextRunDate()->getTimestamp(); + $dispatcher->dispatchEvent($v, null, $cron); + Console::verbose("Cron task {$v->class}:{$v->method} is done, using " . round(microtime(true) - $starttime, 3) . 's'); + if ($pre_next_time !== $cron->getNextRunDate()->getTimestamp()) { // 这一步用于判断运行的Cron是否已经覆盖到下一个运行区间 + if (time() + round($v->check_delay_time / 1000) >= $pre_next_time) { // 假设检测到下一个周期运行时间已经要超过了预计的时间,则警告运行超时 + Console::warning(zm_internal_errcode('E00077') . 'Cron task ' . $v->class . ':' . $v->method . ' is timeout'); + } + } else { + Console::verbose('Next run at ' . date('Y-m-d H:i:s', $cron->getNextRunDate()->getTimestamp())); + } + $v->setRecordNextTime($pre_next_time); + $v->setStatus(2); + } +} diff --git a/src/ZM/Utils/SignalListener.php b/src/ZM/Utils/SignalListener.php index ecd7139f..17cafe4e 100644 --- a/src/ZM/Utils/SignalListener.php +++ b/src/ZM/Utils/SignalListener.php @@ -95,7 +95,7 @@ class SignalListener $name = explode('.', $file); if (end($name) == 'pid' && $name[0] !== 'manager') { $pid = file_get_contents($file_path . '/' . $file); - Process::kill($pid, SIGKILL); + Process::kill((int) $pid, SIGKILL); } unlink($file_path . '/' . $file); }