Files
ulthon_admin/extend/base/common/controller/TimerControllerBase.php
augushong 3db3f7de78 feat(timer): syncConfig concurrency 不变量与控制器 cap 防御
TimerServiceBase syncConfig 不覆盖 DB concurrency(NULL=继承代码默认)。TimerControllerBase cap 改 >= 语义,放行 concurrency_id < cap 的 0-indexed 分片。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-07-25 21:57:39 +08:00

120 lines
3.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace base\common\controller;
use app\admin\model\SystemTimerLog;
use app\common\controller\ToolsController;
use think\facade\Cache;
class TimerControllerBase extends ToolsController
{
protected $frequency = null;
/**
* 最大并发上限 cap.
* 放行 concurrency_id < $concurrency 的请求0-indexed 分片,合法 id ∈ [0, cap-1]
* 放行 concurrency_count <= $concurrency 的请求(并发总数声明,合法 ∈ [1, cap])。
* DB 实际并发通过 T6 有效域钳制 = min(DB concurrency, 控制器 cap, max_handles 预算)
* 动态扩容时高 concurrency_id 只要 < cap 就不会被误杀。
* 子类按任务实际承载力自行设置(默认 1 = 单分片,只允许 id=0
*/
protected $concurrency = 1;
protected $concurrencyId = 0;
protected $hostId = null;
public function initialize()
{
parent::initialize();
$concurrency_id = $this->request->param('concurrency_id', 0);
// cap 防御0-indexed 分片号,合法 id ∈ [0, cap-1]id >= cap 视为越界(超 cap 的分片不被本控制器承载)
if ($concurrency_id >= $this->concurrency) {
$this->error('concurrency id error');
}
$this->concurrencyId = $concurrency_id;
$concurrency_count = $this->request->param('concurrency_count', 1);
// cap 防御:并发总数声明,合法 ∈ [1, cap]count > cap 视为超限DB concurrency 经 T6 钳制后不会超过 cap
if ($concurrency_count > $this->concurrency) {
$this->error('concurrency count error');
}
$this->hostId = $this->request->param('host_id', '');
if (is_int($this->frequency)) {
$this->protectVisit($this->frequency);
}
}
protected function protectVisit(int $frequency)
{
$cache_tag = 'timer_protect';
$cache_key = 'timer_protect_' . md5($this->request->url());
$last_exec_time = Cache::get($cache_key, 0);
if ($last_exec_time >= time() - $frequency) {
return $this->error('请不要频繁请求');
}
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,
'result' => 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, ?string $result = 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->result = $result;
$log->save();
}
}
/**
* 日志包裹入口:自动记录 do() 的执行过程和返回值.
* 子类仍实现 do()URL 路由从 /do 改为 /execute 触发此方法.
*/
public function execute()
{
$taskName = $this->request->param('task_name', '');
$logId = $this->logStart($taskName);
try {
$result = $this->do();
$resultStr = is_string($result) ? $result : json_encode($result, JSON_UNESCAPED_UNICODE);
$this->logEnd($logId, 'success', null, $resultStr);
return $result;
} catch (\Throwable $e) {
$this->logEnd($logId, 'error', $e->getMessage());
throw $e;
}
}
}