mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 15:32:48 +08:00
feat(timer): 新增 run_type 调度、host_id 投递和日志清理
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
This commit is contained in:
@@ -14,6 +14,7 @@ use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
use Workerman\Http\Client as HttpClient;
|
||||
use Workerman\Timer;
|
||||
@@ -100,6 +101,112 @@ class TimerBase extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 run_type 判断当前节点是否应执行该任务(仅对 site 类型生效).
|
||||
* 在 Cache 节流之后、实际执行之前调用。
|
||||
*
|
||||
* @param array|\ArrayAccess $task_item 任务实例(含 name/type/frequency 等)
|
||||
* @return bool true=应执行 false=跳过
|
||||
*/
|
||||
protected function shouldExecuteTask($task_item): bool
|
||||
{
|
||||
// call 类型任务不受 run_type 调度层影响
|
||||
if (($task_item['type'] ?? '') === 'call') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$task_name = $task_item['name'] ?? '';
|
||||
|
||||
try {
|
||||
$config = Db::name('system_timer_config')
|
||||
->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']);
|
||||
}
|
||||
|
||||
34
extend/base/common/command/admin/timer/TimerLogCleanBase.php
Normal file
34
extend/base/common/command/admin/timer/TimerLogCleanBase.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace base\common\command\admin\timer;
|
||||
|
||||
use app\common\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
class TimerLogCleanBase extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
parent::configure();
|
||||
$this->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} 天的定时器执行日志。");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user