diff --git a/src/Module/Example/Hello123.php b/src/Module/Example/Hello123.php index e84dab3b..497391ec 100644 --- a/src/Module/Example/Hello123.php +++ b/src/Module/Example/Hello123.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Module\Example; +use OneBot\Driver\Coroutine\Adaptive; use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent; +use ZM\Annotation\Framework\Cron; use ZM\Annotation\Http\Route; use ZM\Annotation\Middleware\Middleware; use ZM\Annotation\OneBot\BotCommand; @@ -36,4 +38,19 @@ class Hello123 { $context->reply($event->getMessage()); } + + #[Cron('* * * * *', no_overlap: true)] + public function logTime(): void + { + $time = date('Y-m-d H:i:s'); + logger()->info('我看到时间了,让我写下来'); + Adaptive::sleep(5); + logger()->info('写好啦,时间是' . $time); + } + + #[Cron('* * * * *')] + public function logTime2(): void + { + logger()->info('我不需要等,但也不给你看时间'); + } } diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index e4943e58..b8c67648 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -7,10 +7,12 @@ namespace ZM\Annotation; use Doctrine\Common\Annotations\AnnotationReader; use Koriym\Attributes\AttributeReader; use Koriym\Attributes\DualReader; +use ZM\Annotation\Framework\Cron; use ZM\Annotation\Http\Controller; use ZM\Annotation\Http\Route; use ZM\Annotation\Interfaces\ErgodicAnnotation; use ZM\Annotation\Middleware\Middleware; +use ZM\Schedule\Schedule; use ZM\Store\FileSystem; use ZM\Utils\HttpUtil; @@ -56,7 +58,8 @@ class AnnotationParser $this->special_parsers = [ Middleware::class => [function (Middleware $middleware) { \middleware()->bindMiddleware([resolve($middleware->class), $middleware->method], $middleware->name, $middleware->params); }], Route::class => [[$this, 'addRouteAnnotation']], - Closed::class => [function () { return false; }], + Closed::class => [fn () => false], + Cron::class => [[resolve(Schedule::class), 'addSchedule']], ]; } } diff --git a/src/ZM/Annotation/Framework/Cron.php b/src/ZM/Annotation/Framework/Cron.php new file mode 100644 index 00000000..727c5453 --- /dev/null +++ b/src/ZM/Annotation/Framework/Cron.php @@ -0,0 +1,34 @@ +expression = new CronExpression($expression); + } +} diff --git a/src/ZM/Schedule/Schedule.php b/src/ZM/Schedule/Schedule.php new file mode 100644 index 00000000..c785e2d2 --- /dev/null +++ b/src/ZM/Schedule/Schedule.php @@ -0,0 +1,87 @@ +error('排程任务只能在协程环境下使用'); + return; + } + + // 每秒检查一次,精度为一分钟,从最近的下一分钟开始 + $c->create(function () { + /* @phpstan-ignore-next-line 协程会睡觉的,不会阻塞 */ + while (true) { + $now = time(); + $this->run(); + $sleep_time = 60 - ($now % 60); + Adaptive::sleep($sleep_time); + } + }); + } + + /** + * 添加一个排程任务 + * + * @param Cron $cron Cron 注解 + */ + public function addSchedule(Cron $cron): void + { + $this->schedules[] = $cron; + } + + /** + * 获取到期的排程任务 + * + * @return Cron[] + */ + public function due(): array + { + return array_filter($this->schedules, fn (Cron $cron) => $cron->expression->isDue()); + } + + /** + * 运行排程任务 + */ + public function run(): void + { + // 同时运行到期的排程任务 + foreach ($this->due() as $cron) { + // 检查是否重叠执行 + if ($cron->no_overlap && in_array($cron, $this->executing, true)) { + continue; + } + $this->executing[] = $cron; + // 新建一个协程运行排程任务,避免阻塞 + Adaptive::getCoroutine()->create(function () use ($cron) { + $callable = [$cron->class, $cron->method]; + container()->call($callable); + $this->executing = array_diff($this->executing, [$cron]); + }); + } + } +}