From 25fab093fad59611e2e9d156bd1997c9fc2f2677 Mon Sep 17 00:00:00 2001 From: augushong Date: Tue, 26 May 2026 02:41:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(timer):=20=E6=96=B0=E5=A2=9E=20run=5Ftype?= =?UTF-8?q?=20=E8=B0=83=E5=BA=A6=E3=80=81host=5Fid=20=E6=8A=95=E9=80=92?= =?UTF-8?q?=E5=92=8C=E6=97=A5=E5=BF=97=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T7: TimerBase shouldExecuteTask() - main/auto/all/manual/disabled modes with two-phase DB row lock for auto mode T8: TimerControllerBase - host_id param, logStart/logEnd methods TimerServiceBase - inject host_id into site URLs T9: TimerLogClean command - php think admin:timer:log:clean --days=30 --- app/admin/model/SystemTimerLog.php | 12 ++ .../command/admin/timer/TimerLogClean.php | 11 ++ extend/base/common/command/TimerBase.php | 118 ++++++++++++++++++ .../command/admin/timer/TimerLogCleanBase.php | 34 +++++ .../common/controller/TimerControllerBase.php | 36 ++++++ .../base/common/service/TimerServiceBase.php | 2 + extend/think/UlthonAdminService.php | 2 + 7 files changed, 215 insertions(+) create mode 100644 app/admin/model/SystemTimerLog.php create mode 100644 app/common/command/admin/timer/TimerLogClean.php create mode 100644 extend/base/common/command/admin/timer/TimerLogCleanBase.php diff --git a/app/admin/model/SystemTimerLog.php b/app/admin/model/SystemTimerLog.php new file mode 100644 index 0000000..2771d6c --- /dev/null +++ b/app/admin/model/SystemTimerLog.php @@ -0,0 +1,12 @@ +where('task_name', $task_name) + ->find(); + } catch (\Throwable $e) { + Log::warning('shouldExecuteTask: 查询 system_timer_config 失败 - ' . $e->getMessage()); + + return true; + } + + // 无数据库记录时不阻塞(回退到默认行为) + if (empty($config)) { + return true; + } + + // status=0 表示任务已禁用 + if (isset($config['status']) && (int) $config['status'] === 0) { + return false; + } + + $run_type = $config['run_type'] ?? 'auto'; + + switch ($run_type) { + case 'main': + // 仅主节点执行 + $master_node = HostService::getMasterNode(); + $node_id = HostService::getNodeId(); + + return ($master_node === $node_id); + + case 'auto': + // 两阶段 DB 行锁竞争:竞争成功后释放锁,再执行任务 + Db::startTrans(); + try { + $row = Db::name('system_timer_config') + ->where('task_name', $task_name) + ->lock(true) + ->find(); + + if ($row && !empty($row['last_execute_time']) && $row['last_execute_time'] > 0) { + if ((time() - (int) $row['last_execute_time']) < ($task_item['frequency'] ?? 0)) { + Db::commit(); + + return false; // 另一个节点刚执行过 + } + } + + Db::name('system_timer_config') + ->where('task_name', $task_name) + ->update([ + 'last_execute_node' => HostService::getNodeId(), + 'last_execute_time' => time(), + 'update_time' => time(), + ]); + + Db::commit(); + + return true; + } catch (\Throwable $e) { + Db::rollback(); + Log::warning('shouldExecuteTask auto mode exception: ' . $e->getMessage()); + + return false; + } + + case 'all': + // 所有节点都执行 + return true; + + case 'manual': + // 手动触发:检查 manual_trigger=1,执行一次后重置为 0 + if (!empty($config['manual_trigger'])) { + Db::name('system_timer_config') + ->where('task_name', $task_name) + ->update([ + 'manual_trigger' => 0, + 'update_time' => time(), + ]); + + return true; + } + + return false; + + default: + // 未知 run_type 不阻塞 + return true; + } + } + public function runParallel() { // 重新构造命令行参数,以便兼容workerman的命令 @@ -152,6 +259,12 @@ class TimerBase extends Command $request_item['is_running'] = true; $request_item['last_run_time'] = time(); + // run_type 调度检查(节流之后、实际执行之前) + if (!$this->shouldExecuteTask($request_item)) { + $request_item['is_running'] = false; + continue; + } + // $http->request($host . $request_item['target'], [ @@ -231,6 +344,11 @@ class TimerBase extends Command Cache::tag($cache_tag)->set($cache_key, time()); + // run_type 调度检查(Cache 节流之后、实际执行之前) + if (!$this->shouldExecuteTask($request_item)) { + continue; + } + $output->writeln(date('Y-m-d H:i:s') . ': build site request async: ' . $request_item['target']); $list_promises[$request_item['name']] = $client->getAsync($request_item['target']); } diff --git a/extend/base/common/command/admin/timer/TimerLogCleanBase.php b/extend/base/common/command/admin/timer/TimerLogCleanBase.php new file mode 100644 index 0000000..cee4c9f --- /dev/null +++ b/extend/base/common/command/admin/timer/TimerLogCleanBase.php @@ -0,0 +1,34 @@ +setName('admin:timer:log:clean') + ->addOption('days', 'd', Option::VALUE_OPTIONAL, '清理多少天前的日志', 30) + ->setDescription('清理定时器执行日志'); + } + + protected function execute(Input $input, Output $output) + { + $days = (int) $input->getOption('days'); + $threshold = time() - ($days * 86400); + + $count = Db::name('system_timer_log') + ->where('create_time', '<', $threshold) + ->delete(); + + $output->writeln("已清理 {$count} 条超过 {$days} 天的定时器执行日志。"); + } +} diff --git a/extend/base/common/controller/TimerControllerBase.php b/extend/base/common/controller/TimerControllerBase.php index b9ac60b..464226e 100644 --- a/extend/base/common/controller/TimerControllerBase.php +++ b/extend/base/common/controller/TimerControllerBase.php @@ -2,6 +2,7 @@ namespace base\common\controller; +use app\admin\model\SystemTimerLog; use app\common\controller\ToolsController; use think\facade\Cache; @@ -13,6 +14,8 @@ class TimerControllerBase extends ToolsController protected $concurrencyId = 0; + protected $hostId = null; + public function initialize() { parent::initialize(); @@ -28,6 +31,8 @@ class TimerControllerBase extends ToolsController $this->error('concurrency count error'); } + $this->hostId = $this->request->param('host_id', ''); + if (is_int($this->frequency)) { $this->protectVisit($this->frequency); } @@ -47,4 +52,35 @@ class TimerControllerBase extends ToolsController Cache::tag($cache_tag)->set($cache_key, time()); } + + public function logStart(string $taskName): int + { + $data = [ + 'task_name' => $taskName, + 'node_id' => $this->hostId, + 'run_type' => '', + 'start_time' => time(), + 'end_time' => 0, + 'duration' => 0, + 'status' => 'running', + 'error_message' => null, + 'concurrency_id' => $this->concurrencyId, + 'create_time' => time(), + ]; + $log = SystemTimerLog::create($data); + return $log->id; + } + + public function logEnd(int $logId, string $status = 'success', ?string $errorMessage = null): void + { + $log = SystemTimerLog::find($logId); + if ($log) { + $endTime = time(); + $log->end_time = $endTime; + $log->duration = ($endTime - $log->start_time) * 1000; // ms + $log->status = $status; + $log->error_message = $errorMessage; + $log->save(); + } + } } diff --git a/extend/base/common/service/TimerServiceBase.php b/extend/base/common/service/TimerServiceBase.php index 1bc7a23..5b2fd7c 100644 --- a/extend/base/common/service/TimerServiceBase.php +++ b/extend/base/common/service/TimerServiceBase.php @@ -4,6 +4,7 @@ namespace base\common\service; use app\admin\model\SystemTimerConfig; use app\common\model\VirtualModel; +use app\common\service\HostService; class TimerServiceBase { @@ -117,6 +118,7 @@ class TimerServiceBase $params = [ 'concurrency_id' => $i, 'concurrency_count' => $concurrency, + 'host_id' => HostService::getNodeId(), ]; // 处理target if ($config_item['type'] == 'site') { diff --git a/extend/think/UlthonAdminService.php b/extend/think/UlthonAdminService.php index 0326dd3..4038160 100644 --- a/extend/think/UlthonAdminService.php +++ b/extend/think/UlthonAdminService.php @@ -26,6 +26,7 @@ use app\common\command\admin\role\AdminRolePermissionAssign; use app\common\command\admin\role\AdminRolePermissionList; use app\common\command\admin\role\AdminRolePermissionRevoke; use app\common\command\admin\stack\AdminStackMode; +use app\common\command\admin\timer\TimerLogClean; use app\common\command\admin\user\AdminUserRoleAssign; use app\common\command\admin\user\AdminUserRoleList; use app\common\command\admin\user\AdminUserRoleRevoke; @@ -116,6 +117,7 @@ class UlthonAdminService extends Service AdminRolePermissionRevoke::class, AdminRolePermissionList::class, AdminStackMode::class, + TimerLogClean::class, AdminUserRoleAssign::class, AdminUserRoleRevoke::class, AdminUserRoleList::class,