e
This commit is contained in:
+4
-5
@@ -14,11 +14,10 @@
|
||||
"ext-swoole": "*",
|
||||
"ext-redis": "*",
|
||||
"psr/log": "^1.0",
|
||||
"psr/event-dispatcher": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/console": "如需集成 kiri-core 或使用命令行管理,建议安装 ^v8.0",
|
||||
"game-worker/kiri-core": "如需集成到 kiri-core 框架,建议安装 kiri-core"
|
||||
"psr/event-dispatcher": "^1.0",
|
||||
"symfony/console": "^v8.0",
|
||||
"game-worker/kiri-core": "^v1.0",
|
||||
"game-worker/kiri-http-server": "^v1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
+3
-3
@@ -16,8 +16,8 @@ return [
|
||||
|
||||
// 调度器配置
|
||||
'scheduler' => [
|
||||
// 调度器 tick 间隔 (秒)
|
||||
'tick_interval' => (int)env('CRONTAB_TICK_INTERVAL', 1),
|
||||
// 调度器 tick 间隔 (秒,支持小数;毫秒级任务建议 0.05~0.1)
|
||||
'tick_interval' => (float)env('CRONTAB_TICK_INTERVAL', 0.1),
|
||||
// 任务执行超时时间 (秒)
|
||||
'task_timeout' => (int)env('CRONTAB_TASK_TIMEOUT', 300),
|
||||
// 主锁 TTL (秒)
|
||||
@@ -36,7 +36,7 @@ return [
|
||||
|
||||
// 注册的任务列表 (配置模式,expression 字符串格式)
|
||||
// 注解模式通过 #[Crontab] 在 handle() 方法上声明,kiri-core Scanner 自动发现
|
||||
// 表达式: every:60 | every:5m | every:1h | daily:03:00 | hourly:30 | cron:*\/5 * * * * | at:时间戳
|
||||
// 表达式: every:100ms | every:60 | every:5m | every:1h | daily:03:00 | hourly:30 | cron:*\/5 * * * * | at:时间戳
|
||||
// 每个任务需实现 TaskInterface 接口
|
||||
'tasks' => [
|
||||
// 示例:
|
||||
|
||||
+98
-48
@@ -14,40 +14,70 @@ namespace Kiri\Crontab;
|
||||
* N-M 范围匹配
|
||||
*
|
||||
* 自定义表达式:
|
||||
* every:{毫秒}ms
|
||||
* every:{秒}
|
||||
* every:{秒}s
|
||||
* every:{分}m
|
||||
* every:{时}h
|
||||
* daily:{HH:MM}
|
||||
* hourly:{MM}
|
||||
* at:{时间戳}
|
||||
* at:{时间戳秒|时间戳毫秒|时间戳ms}
|
||||
*/
|
||||
class CronExpression
|
||||
{
|
||||
|
||||
/**
|
||||
* 计算给定时间戳之后的下次执行时间
|
||||
* 计算给定秒级时间戳之后的下次执行时间。
|
||||
*
|
||||
* 保留秒级返回值用于兼容旧调用;调度器应使用 getNextRunTimeMs() 获得毫秒级时间。
|
||||
*
|
||||
* @param string $expression 调度表达式
|
||||
* @param int $afterTimestamp 起始时间戳 (不含)
|
||||
* @return int 下次执行时间戳,一次性任务过期返回 0
|
||||
* @param int $afterTimestamp 起始秒级时间戳 (不含)
|
||||
* @return int 下次执行秒级时间戳,一次性任务过期返回 0
|
||||
*/
|
||||
public function getNextRunTime(string $expression, int $afterTimestamp = 0): int
|
||||
{
|
||||
if ($afterTimestamp <= 0) {
|
||||
$afterTimestamp = time();
|
||||
$afterTimestampMs = $afterTimestamp > 0 ? $afterTimestamp * 1000 : self::currentTimeMs();
|
||||
$nextRunMs = $this->getNextRunTimeMs($expression, $afterTimestampMs);
|
||||
|
||||
if ($nextRunMs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return intdiv($nextRunMs, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算给定毫秒级时间戳之后的下次执行时间。
|
||||
*
|
||||
* @param string $expression 调度表达式
|
||||
* @param int $afterTimestampMs 起始毫秒级时间戳 (不含)
|
||||
* @return int 下次执行毫秒级时间戳,一次性任务过期返回 0
|
||||
*/
|
||||
public function getNextRunTimeMs(string $expression, int $afterTimestampMs = 0): int
|
||||
{
|
||||
if ($afterTimestampMs <= 0) {
|
||||
$afterTimestampMs = self::currentTimeMs();
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
str_starts_with($expression, 'every:') => $this->parseEvery($expression, $afterTimestamp),
|
||||
str_starts_with($expression, 'daily:') => $this->parseDaily($expression, $afterTimestamp),
|
||||
str_starts_with($expression, 'hourly:') => $this->parseHourly($expression, $afterTimestamp),
|
||||
str_starts_with($expression, 'at:') => $this->parseAt($expression, $afterTimestamp),
|
||||
str_starts_with($expression, 'cron:') => $this->parseCron(substr($expression, 5), $afterTimestamp),
|
||||
default => $this->parseCron($expression, $afterTimestamp),
|
||||
str_starts_with($expression, 'every:') => $this->parseEveryMs($expression, $afterTimestampMs),
|
||||
str_starts_with($expression, 'daily:') => $this->parseDailyMs($expression, $afterTimestampMs),
|
||||
str_starts_with($expression, 'hourly:') => $this->parseHourlyMs($expression, $afterTimestampMs),
|
||||
str_starts_with($expression, 'at:') => $this->parseAtMs($expression, $afterTimestampMs),
|
||||
str_starts_with($expression, 'cron:') => $this->parseCronMs(substr($expression, 5), $afterTimestampMs),
|
||||
default => $this->parseCronMs($expression, $afterTimestampMs),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前毫秒级 Unix 时间戳。
|
||||
*/
|
||||
public static function currentTimeMs(): int
|
||||
{
|
||||
return (int)floor(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表达式的可读间隔描述
|
||||
*/
|
||||
@@ -72,22 +102,39 @@ class CronExpression
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 every:{N}[s|m|h] 表达式
|
||||
* 解析 every:{N}[ms|s|m|h] 表达式,返回毫秒级下次执行时间。
|
||||
*/
|
||||
private function parseEvery(string $expression, int $afterTimestamp): int
|
||||
private function parseEveryMs(string $expression, int $afterTimestampMs): int
|
||||
{
|
||||
$value = substr($expression, 6);
|
||||
$intervalMs = $this->parseIntervalMs(substr($expression, 6));
|
||||
if ($intervalMs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$seconds = match (true) {
|
||||
str_ends_with($value, 's') => (int)$value,
|
||||
str_ends_with($value, 'm') => (int)$value * 60,
|
||||
str_ends_with($value, 'h') => (int)$value * 3600,
|
||||
default => (int)$value,
|
||||
};
|
||||
return $afterTimestampMs + $intervalMs;
|
||||
}
|
||||
|
||||
// 从上次调度时间累加,避免时间漂移和积压
|
||||
// 如果 afterTimestamp 距离上次调度超过 n 个周期,只加一个周期(按第一次调度时间对齐)
|
||||
return $afterTimestamp + $seconds;
|
||||
/**
|
||||
* 将 every 值解析成毫秒;无单位保持兼容,表示秒。
|
||||
*/
|
||||
private function parseIntervalMs(string $value): int
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if (str_ends_with($value, 'ms')) {
|
||||
return max(0, (int)substr($value, 0, -2));
|
||||
}
|
||||
if (str_ends_with($value, 's')) {
|
||||
return max(0, (int)round((float)substr($value, 0, -1) * 1000));
|
||||
}
|
||||
if (str_ends_with($value, 'm')) {
|
||||
return max(0, (int)round((float)substr($value, 0, -1) * 60000));
|
||||
}
|
||||
if (str_ends_with($value, 'h')) {
|
||||
return max(0, (int)round((float)substr($value, 0, -1) * 3600000));
|
||||
}
|
||||
|
||||
return max(0, (int)round((float)$value * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +144,7 @@ class CronExpression
|
||||
{
|
||||
$value = substr($expression, 6);
|
||||
return match (true) {
|
||||
str_ends_with($value, 'ms') => '每 ' . ((int)$value) . ' 毫秒',
|
||||
str_ends_with($value, 's') => '每 ' . ((int)$value) . ' 秒',
|
||||
str_ends_with($value, 'm') => '每 ' . ((int)$value) . ' 分钟',
|
||||
str_ends_with($value, 'h') => '每 ' . ((int)$value) . ' 小时',
|
||||
@@ -107,7 +155,7 @@ class CronExpression
|
||||
/**
|
||||
* 解析 daily:{HH:MM} 表达式
|
||||
*/
|
||||
private function parseDaily(string $expression, int $afterTimestamp): int
|
||||
private function parseDailyMs(string $expression, int $afterTimestampMs): int
|
||||
{
|
||||
$timeStr = substr($expression, 6);
|
||||
$parts = explode(':', $timeStr);
|
||||
@@ -115,54 +163,61 @@ class CronExpression
|
||||
$minute = (int)($parts[1] ?? 0);
|
||||
$second = (int)($parts[2] ?? 0);
|
||||
|
||||
$afterTimestamp = intdiv($afterTimestampMs, 1000);
|
||||
$currentDate = getdate($afterTimestamp);
|
||||
$targetTime = mktime($hour, $minute, $second, $currentDate['mon'], $currentDate['mday'], $currentDate['year']);
|
||||
|
||||
if ($targetTime <= $afterTimestamp) {
|
||||
// 今天的时间已过,推到明天
|
||||
$targetTime = $targetTime + 86400;
|
||||
if ($targetTime * 1000 <= $afterTimestampMs) {
|
||||
$targetTime += 86400;
|
||||
}
|
||||
|
||||
return $targetTime;
|
||||
return $targetTime * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 hourly:{MM} 表达式
|
||||
*/
|
||||
private function parseHourly(string $expression, int $afterTimestamp): int
|
||||
private function parseHourlyMs(string $expression, int $afterTimestampMs): int
|
||||
{
|
||||
$minute = (int)substr($expression, 7);
|
||||
$afterTimestamp = intdiv($afterTimestampMs, 1000);
|
||||
$currentDate = getdate($afterTimestamp);
|
||||
$targetTime = mktime($currentDate['hours'], $minute, 0, $currentDate['mon'], $currentDate['mday'], $currentDate['year']);
|
||||
|
||||
if ($targetTime <= $afterTimestamp) {
|
||||
$targetTime = $targetTime + 3600;
|
||||
if ($targetTime * 1000 <= $afterTimestampMs) {
|
||||
$targetTime += 3600;
|
||||
}
|
||||
|
||||
return $targetTime;
|
||||
return $targetTime * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 at:{时间戳} 表达式 — 一次性任务
|
||||
*/
|
||||
private function parseAt(string $expression, int $afterTimestamp): int
|
||||
private function parseAtMs(string $expression, int $afterTimestampMs): int
|
||||
{
|
||||
$timestamp = (int)substr($expression, 3);
|
||||
if ($timestamp <= $afterTimestamp) {
|
||||
// 已过期,返回 0 表示不再调度
|
||||
$value = trim(substr($expression, 3));
|
||||
$timestampMs = str_ends_with($value, 'ms')
|
||||
? (int)substr($value, 0, -2)
|
||||
: (int)$value;
|
||||
|
||||
if ($timestampMs > 0 && $timestampMs < 1000000000000) {
|
||||
$timestampMs *= 1000;
|
||||
}
|
||||
|
||||
if ($timestampMs <= $afterTimestampMs) {
|
||||
return 0;
|
||||
}
|
||||
return $timestamp;
|
||||
return $timestampMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析标准 5 字段 cron 表达式: 分 时 日 月 周
|
||||
*/
|
||||
private function parseCron(string $cronExpression, int $afterTimestamp): int
|
||||
private function parseCronMs(string $cronExpression, int $afterTimestampMs): int
|
||||
{
|
||||
$fields = preg_split('/\s+/', trim($cronExpression));
|
||||
if (count($fields) !== 5) {
|
||||
// 格式无效,返回 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -172,8 +227,8 @@ class CronExpression
|
||||
$month = $fields[3];
|
||||
$weekday = $fields[4];
|
||||
|
||||
// 从 afterTimestamp 下一秒开始逐分钟搜索,最多搜索 2 年
|
||||
$searchStart = $afterTimestamp + 60;
|
||||
$afterTimestamp = intdiv($afterTimestampMs, 1000);
|
||||
$searchStart = ((int)floor($afterTimestamp / 60) + 1) * 60;
|
||||
$searchEnd = $afterTimestamp + 365 * 2 * 86400;
|
||||
|
||||
for ($ts = $searchStart; $ts <= $searchEnd; $ts += 60) {
|
||||
@@ -187,7 +242,7 @@ class CronExpression
|
||||
$matched = $matched && $this->matchCronField($weekday, $t['wday'], 0, 6);
|
||||
|
||||
if ($matched) {
|
||||
return $ts;
|
||||
return $ts * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,12 +259,10 @@ class CronExpression
|
||||
*/
|
||||
private function matchCronField(string $fieldValue, int $current, int $min, int $max): bool
|
||||
{
|
||||
// * 匹配所有值
|
||||
if ($fieldValue === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// *\/N 每隔 N 步进
|
||||
if (str_starts_with($fieldValue, '*/')) {
|
||||
$step = (int)substr($fieldValue, 2);
|
||||
if ($step <= 0) {
|
||||
@@ -218,7 +271,6 @@ class CronExpression
|
||||
return ($current - $min) % $step === 0;
|
||||
}
|
||||
|
||||
// 逗号分隔的枚举值
|
||||
if (str_contains($fieldValue, ',')) {
|
||||
$values = explode(',', $fieldValue);
|
||||
foreach ($values as $val) {
|
||||
@@ -229,7 +281,6 @@ class CronExpression
|
||||
return false;
|
||||
}
|
||||
|
||||
// N-M 范围值
|
||||
if (str_contains($fieldValue, '-')) {
|
||||
$parts = explode('-', $fieldValue);
|
||||
$rangeStart = (int)$parts[0];
|
||||
@@ -237,7 +288,6 @@ class CronExpression
|
||||
return $current >= $rangeStart && $current <= $rangeEnd;
|
||||
}
|
||||
|
||||
// 精确值
|
||||
return (int)$fieldValue === $current;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class CrontabProcess extends AbstractProcess
|
||||
$redis = $this->createRedisConnection();
|
||||
$cronExpression = new CronExpression;
|
||||
$schedulerConfig = $this->config['scheduler'] ?? [];
|
||||
$this->scheduler = new CrontabScheduler(redis: $redis, cronExpression: $cronExpression, logger: $this->logger, tickInterval: (int)($schedulerConfig['tick_interval'] ?? 1), taskTimeout: (int)($schedulerConfig['task_timeout'] ?? 300), lockTtl: (int)($schedulerConfig['lock_ttl'] ?? 60), lockRenewInterval: (int)($schedulerConfig['lock_renew_interval'] ?? 15), concurrentTasks: (bool)($schedulerConfig['concurrent_tasks'] ?? true), maxConcurrent: (int)($schedulerConfig['max_concurrent'] ?? 10),);
|
||||
$this->scheduler = new CrontabScheduler(redis: $redis, cronExpression: $cronExpression, logger: $this->logger, tickInterval: (float)($schedulerConfig['tick_interval'] ?? 0.1), taskTimeout: (int)($schedulerConfig['task_timeout'] ?? 300), lockTtl: (int)($schedulerConfig['lock_ttl'] ?? 60), lockRenewInterval: (int)($schedulerConfig['lock_renew_interval'] ?? 15), concurrentTasks: (bool)($schedulerConfig['concurrent_tasks'] ?? true), maxConcurrent: (int)($schedulerConfig['max_concurrent'] ?? 10),);
|
||||
|
||||
$this->scheduler->start();
|
||||
}
|
||||
|
||||
+80
-20
@@ -46,7 +46,7 @@ class CrontabScheduler extends Component
|
||||
private const RUNNING_SET_KEY = 'crontab:running';
|
||||
|
||||
/** @var int 默认 tick 间隔 (秒) */
|
||||
private const DEFAULT_TICK_INTERVAL = 1;
|
||||
private const DEFAULT_TICK_INTERVAL = 0.1;
|
||||
|
||||
/** @var int 默认任务执行超时 (秒) */
|
||||
private const DEFAULT_TASK_TIMEOUT = 300;
|
||||
@@ -68,11 +68,14 @@ class CrontabScheduler extends Component
|
||||
/** @var string|null 非协程环境下当前任务 key 回退存储 */
|
||||
private ?string $fallbackCurrentTaskKey = null;
|
||||
|
||||
/** @var array<string, string> 当前进程持有的任务锁 token */
|
||||
private array $taskLockTokens = [];
|
||||
|
||||
/**
|
||||
* @param \Redis $redis Redis 客户端
|
||||
* @param CronExpression $cronExpression Cron 表达式解析器
|
||||
* @param LoggerInterface|null $logger 日志记录器(容器不可用时的兜底)
|
||||
* @param int $tickInterval tick 间隔 (秒)
|
||||
* @param float $tickInterval tick 间隔 (秒,支持小数)
|
||||
* @param int $taskTimeout 任务执行超时 (秒)
|
||||
* @param int $lockTtl 主锁 TTL (秒)
|
||||
* @param int $lockRenewInterval 主锁续期间隔 (秒)
|
||||
@@ -83,7 +86,7 @@ class CrontabScheduler extends Component
|
||||
private \Redis $redis,
|
||||
private CronExpression $cronExpression,
|
||||
?LoggerInterface $logger = null,
|
||||
private int $tickInterval = self::DEFAULT_TICK_INTERVAL,
|
||||
private float $tickInterval = self::DEFAULT_TICK_INTERVAL,
|
||||
private int $taskTimeout = self::DEFAULT_TASK_TIMEOUT,
|
||||
private int $lockTtl = self::DEFAULT_LOCK_TTL,
|
||||
private int $lockRenewInterval = 15,
|
||||
@@ -221,7 +224,11 @@ class CrontabScheduler extends Component
|
||||
|
||||
$this->removeFromQueue($taskKey);
|
||||
$this->redis->del($hashKey);
|
||||
$this->clearRemovalFlag($taskKey);
|
||||
|
||||
if (!$this->isTaskRunning($taskKey)) {
|
||||
$this->redis->del($this->getTaskLockKey($taskKey));
|
||||
}
|
||||
|
||||
$this->logInfo("[CrontabScheduler] 任务已取消: {$taskKey}");
|
||||
|
||||
@@ -326,7 +333,9 @@ class CrontabScheduler extends Component
|
||||
$taskKey = substr($hashKey, strlen(self::KEY_PREFIX . ':task:'));
|
||||
$this->removeFromQueue($taskKey);
|
||||
$this->redis->del($hashKey);
|
||||
if (!$this->isTaskRunning($taskKey)) {
|
||||
$this->redis->del($this->getTaskLockKey($taskKey));
|
||||
}
|
||||
$this->redis->del('crontab:removal:' . $taskKey);
|
||||
$removedCount++;
|
||||
}
|
||||
@@ -344,7 +353,7 @@ class CrontabScheduler extends Component
|
||||
foreach (TaskRegistry::all() as $taskKey => $taskConfig) {
|
||||
$hashKey = $this->getTaskHashKey($taskKey);
|
||||
|
||||
$nextRun = $this->cronExpression->getNextRunTime($taskConfig->expression, time() - 1);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($taskConfig->expression, $this->currentTimeMs() - 1);
|
||||
$interval = $this->cronExpression->getIntervalDescription($taskConfig->expression);
|
||||
|
||||
$taskConfig->nextRun = $nextRun;
|
||||
@@ -388,7 +397,7 @@ class CrontabScheduler extends Component
|
||||
}
|
||||
|
||||
$config = TaskConfig::fromHash($taskKey, $hash);
|
||||
$nextRun = $this->cronExpression->getNextRunTime($config->expression, time() - 1);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($config->expression, $this->currentTimeMs() - 1);
|
||||
|
||||
$this->redis->hMSet($hashKey, [
|
||||
'status' => 'active',
|
||||
@@ -462,7 +471,7 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function processDueTasks(): void
|
||||
{
|
||||
$now = time();
|
||||
$now = $this->currentTimeMs();
|
||||
|
||||
$dueTaskKeys = $this->redis->zRangeByScore(
|
||||
self::QUEUE_KEY,
|
||||
@@ -602,15 +611,15 @@ class CrontabScheduler extends Component
|
||||
} finally {
|
||||
$this->redis->sRem(self::RUNNING_SET_KEY, $taskKey);
|
||||
|
||||
// 检查是否被标记为"执行后移除"
|
||||
$shouldRemove = $this->isTaskMarkedForRemoval($taskKey);
|
||||
// 任务执行期间可能被外部取消或配置同步删除,不允许执行结束后重新写回。
|
||||
$taskExists = (bool)$this->redis->exists($hashKey);
|
||||
$shouldRemove = !$taskExists || $this->isTaskMarkedForRemoval($taskKey);
|
||||
|
||||
if ($shouldRemove) {
|
||||
$this->removeFromQueue($taskKey);
|
||||
$this->redis->del($hashKey);
|
||||
$this->redis->del($this->getTaskLockKey($taskKey));
|
||||
$this->clearRemovalFlag($taskKey);
|
||||
$this->logInfo("[CrontabScheduler] 任务已自毁: {$taskKey}");
|
||||
$this->logInfo($taskExists ? "[CrontabScheduler] 任务已自毁: {$taskKey}" : "[CrontabScheduler] 任务执行期间已被删除: {$taskKey}");
|
||||
} else {
|
||||
$this->finalizeTaskScheduling($config, $now);
|
||||
}
|
||||
@@ -625,7 +634,7 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function finalizeTaskSuccess(TaskConfig $config, int $now, float $duration): void
|
||||
{
|
||||
$nextRun = $this->cronExpression->getNextRunTime($config->expression, $now);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($config->expression, $now);
|
||||
$isOneShot = $this->cronExpression->isOneShot($config->expression);
|
||||
|
||||
$this->dispatchEvent(new OnTaskExecuted(
|
||||
@@ -642,7 +651,7 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function finalizeTaskFailure(TaskConfig $config, int $now, float $duration, \Throwable $error): void
|
||||
{
|
||||
$nextRun = $this->cronExpression->getNextRunTime($config->expression, $now);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($config->expression, $now);
|
||||
|
||||
$this->dispatchEvent(new OnTaskFailed(
|
||||
taskKey: $config->taskKey,
|
||||
@@ -671,7 +680,7 @@ class CrontabScheduler extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$nextRun = $this->cronExpression->getNextRunTime($config->expression, $now);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($config->expression, $now);
|
||||
$interval = $this->cronExpression->getIntervalDescription($config->expression);
|
||||
|
||||
$updateData = [
|
||||
@@ -738,7 +747,12 @@ class CrontabScheduler extends Component
|
||||
private function acquireTaskLock(string $taskKey): bool
|
||||
{
|
||||
$lockKey = $this->getTaskLockKey($taskKey);
|
||||
return $this->redis->set($lockKey, (string)time(), ['nx', 'ex' => $this->taskTimeout]);
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$locked = $this->redis->set($lockKey, $token, ['nx', 'ex' => $this->taskTimeout]);
|
||||
if ($locked) {
|
||||
$this->taskLockTokens[$taskKey] = $token;
|
||||
}
|
||||
return (bool)$locked;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -746,7 +760,28 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function releaseTaskLock(string $taskKey): void
|
||||
{
|
||||
$this->redis->del($this->getTaskLockKey($taskKey));
|
||||
$token = $this->taskLockTokens[$taskKey] ?? null;
|
||||
unset($this->taskLockTokens[$taskKey]);
|
||||
|
||||
if ($token === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$script = <<<'LUA'
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
end
|
||||
return 0
|
||||
LUA;
|
||||
$this->redis->eval($script, [$this->getTaskLockKey($taskKey), $token], 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断任务是否正在执行。
|
||||
*/
|
||||
private function isTaskRunning(string $taskKey): bool
|
||||
{
|
||||
return (bool)$this->redis->sIsMember(self::RUNNING_SET_KEY, $taskKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -762,7 +797,7 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function persistNewTask(TaskConfig $taskConfig): void
|
||||
{
|
||||
$nextRun = $this->cronExpression->getNextRunTime($taskConfig->expression, time() - 1);
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($taskConfig->expression, $this->currentTimeMs() - 1);
|
||||
$interval = $this->cronExpression->getIntervalDescription($taskConfig->expression);
|
||||
|
||||
$taskConfig->nextRun = $nextRun;
|
||||
@@ -776,7 +811,7 @@ class CrontabScheduler extends Component
|
||||
$this->redis->zAdd(self::QUEUE_KEY, $nextRun, $taskConfig->taskKey);
|
||||
}
|
||||
|
||||
$this->logInfo("[CrontabScheduler] 新任务已注册: {$taskConfig->taskKey} 下次执行: " . date('Y-m-d H:i:s', $nextRun));
|
||||
$this->logInfo("[CrontabScheduler] 新任务已注册: {$taskConfig->taskKey} 下次执行: " . date('Y-m-d H:i:s', intdiv($nextRun, 1000)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,12 +820,31 @@ class CrontabScheduler extends Component
|
||||
private function mergeExistingTask(TaskConfig $taskConfig): void
|
||||
{
|
||||
$hashKey = $this->getTaskHashKey($taskConfig->taskKey);
|
||||
$hash = $this->redis->hGetAll($hashKey);
|
||||
$expressionChanged = ($hash['expression'] ?? '') !== $taskConfig->expression;
|
||||
$statusChanged = ($hash['status'] ?? 'active') !== $taskConfig->status;
|
||||
|
||||
$updateData = [
|
||||
'class' => $taskConfig->className,
|
||||
'name' => $taskConfig->name,
|
||||
'expression' => $taskConfig->expression,
|
||||
'status' => $taskConfig->status,
|
||||
];
|
||||
|
||||
if ($expressionChanged || $statusChanged) {
|
||||
$nextRun = $this->cronExpression->getNextRunTimeMs($taskConfig->expression, $this->currentTimeMs() - 1);
|
||||
$interval = $this->cronExpression->getIntervalDescription($taskConfig->expression);
|
||||
|
||||
$updateData['next_run'] = $nextRun;
|
||||
$updateData['interval'] = $interval;
|
||||
|
||||
if ($taskConfig->status === 'active' && $nextRun > 0) {
|
||||
$this->redis->zAdd(self::QUEUE_KEY, $nextRun, $taskConfig->taskKey);
|
||||
} else {
|
||||
$this->removeFromQueue($taskConfig->taskKey);
|
||||
}
|
||||
}
|
||||
|
||||
$this->redis->hMSet($hashKey, $updateData);
|
||||
}
|
||||
|
||||
@@ -814,9 +868,15 @@ class CrontabScheduler extends Component
|
||||
*/
|
||||
private function calculateNextTickTime(): float
|
||||
{
|
||||
$now = microtime(true);
|
||||
$current = floor($now);
|
||||
return $current + $this->tickInterval;
|
||||
return microtime(true) + $this->tickInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前毫秒级 Unix 时间戳。
|
||||
*/
|
||||
private function currentTimeMs(): int
|
||||
{
|
||||
return CronExpression::currentTimeMs();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ class OnTaskExecuted
|
||||
* @param string $className 任务处理类
|
||||
* @param string $taskName 任务显示名称
|
||||
* @param float $duration 执行耗时 (秒)
|
||||
* @param int $nextRun 下次执行时间戳
|
||||
* @param int $nextRun 下次执行毫秒级时间戳
|
||||
*/
|
||||
public function __construct(
|
||||
public string $taskKey,
|
||||
|
||||
@@ -15,7 +15,7 @@ class OnTaskFailed
|
||||
* @param string $taskName 任务显示名称
|
||||
* @param \Throwable $error 异常信息
|
||||
* @param float $duration 执行耗时 (秒)
|
||||
* @param int $nextRun 下次执行时间戳 (失败仍会调度下次)
|
||||
* @param int $nextRun 下次执行毫秒级时间戳 (失败仍会调度下次)
|
||||
*/
|
||||
public function __construct(
|
||||
public string $taskKey,
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ class TaskConfig
|
||||
* @param string $name 任务显示名称
|
||||
* @param string $expression 调度表达式
|
||||
* @param string $status 状态: active / paused / disabled
|
||||
* @param int $nextRun 下次执行时间戳 (0 表示立即)
|
||||
* @param int $lastRun 上次执行时间戳
|
||||
* @param int $nextRun 下次执行毫秒级时间戳 (0 表示立即)
|
||||
* @param int $lastRun 上次执行毫秒级时间戳
|
||||
* @param string $interval 可读的执行间隔描述
|
||||
* @param int $createdAt 创建时间戳
|
||||
*/
|
||||
|
||||
@@ -70,6 +70,25 @@ class CronExpressionTest
|
||||
$next = $this->parser->getNextRunTime('every:60', $now);
|
||||
$this->assertTrue($next === $now + 60, "期望 " . ($now + 60) . " 实际 $next");
|
||||
}
|
||||
/**
|
||||
* 测试毫秒级 every:100ms 表达式
|
||||
*/
|
||||
public function testEveryMilliseconds(): void
|
||||
{
|
||||
$nowMs = 1760000000123;
|
||||
$next = $this->parser->getNextRunTimeMs('every:100ms', $nowMs);
|
||||
$this->assertTrue($next === $nowMs + 100, "期望 " . ($nowMs + 100) . " 实际 $next");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试秒级表达式在毫秒级接口中返回毫秒时间戳
|
||||
*/
|
||||
public function testEverySecondsAsMilliseconds(): void
|
||||
{
|
||||
$nowMs = 1760000000123;
|
||||
$next = $this->parser->getNextRunTimeMs('every:1s', $nowMs);
|
||||
$this->assertTrue($next === $nowMs + 1000, "期望 " . ($nowMs + 1000) . " 实际 $next");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 every:5m 表达式
|
||||
@@ -159,6 +178,13 @@ class CronExpressionTest
|
||||
$expected = mktime(10, 5, 0, 1, 15, 2026);
|
||||
$this->assertTrue($next === $expected, "期望 $expected 实际 $next");
|
||||
}
|
||||
public function testCronAlignsToMinuteBoundary(): void
|
||||
{
|
||||
$now = mktime(10, 2, 30, 1, 15, 2026); // 10:02:30
|
||||
$next = $this->parser->getNextRunTime('cron:*/5 * * * *', $now);
|
||||
$expected = mktime(10, 5, 0, 1, 15, 2026);
|
||||
$this->assertTrue($next === $expected, "期望 $expected 实际 $next");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 cron 表达式精确时间
|
||||
|
||||
Reference in New Issue
Block a user