Laravel Schedule 中的 dailyAt 是如何工作的
业务逻辑中通过 dailyAt
指定了一个每天都需要执行的定时任务:
$schedule->call(function () {
// 业务逻辑
})->dailyAt('14:29');
Illuminate\Console\Scheduling\ManagesFrequencies
中的 dailyAt
方法,最终是生成 cron 表达式:29 14 * * *
public function dailyAt($time)
{
$segments = explode(':', $time);
return $this->spliceIntoPosition(2, (int) $segments[0])
->spliceIntoPosition(1, count($segments) === 2 ? (int) $segments[1] : '0');
}
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments[$position - 1] = $value;
return $this->cron(implode(' ', $segments));
}
public function cron($expression)
{
// 默认 expression = '* * * * *';
$this->expression = $expression;
return $this;
}
Illuminate\Console\Scheduling\ScheduleRunCommand
中的 handle
方法:
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if (! $this->eventsRan) {
$this->info('No scheduled commands are ready to run.');
}
}
通过 $this->schedule->dueEvents($this->laravel)
获取到期的定时任务,Illuminate\Console\Scheduling\Schedule
中的 dueEvents
:
public function dueEvents($app)
{
return collect($this->events)->filter->isDue($app);
}
Illuminate\Console\Scheduling\Event
中的 isDue
和 expressionPasses
:
public function isDue($app)
{
if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
return false;
}
return $this->expressionPasses() &&
$this->runsInEnvironment($app->environment());
}
protected function expressionPasses()
{
$date = Date::now();
if ($this->timezone) {
$date = $date->setTimezone($this->timezone);
}
return (new CronExpression($this->expression))->isDue($date->toDateTimeString());
}
Cron\CronExpression
中的 isDue
方法的最后 $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
决定是否需要执行:
public function isDue($currentTime = 'now', $timeZone = null): bool
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
Assert::isInstanceOf($currentTime, DateTime::class);
$currentTime->setTimezone(new DateTimeZone($timeZone));
// drop the seconds to 0
$currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);
try {
return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
} catch (Exception $e) {
return false;
}
}
currentTime
就是当前的时间,getNextRunDate
是根据当前时间和 expression
(本例中是:29 14 * * *
)计算出下一次任务需要执行的时间点:
public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentDate = new DateTime($currentTime);
} else {
$currentDate = new DateTime('now');
}
Assert::isInstanceOf($currentDate, DateTime::class);
$currentDate->setTimezone(new DateTimeZone($timeZone));
$currentDate->setTime((int) $currentDate->format('H'), (int) $currentDate->format('i'), 0);
$nextRun = clone $currentDate;
\Log::info('init nextRun = ' . $nextRun->format('Y-m-d H:i:s'));
// We don't have to satisfy * or null fields
$parts = [];
$fields = [];
// [5,3,2,4,1,0]
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
// parts = [1 => 14, 0 => 29]
// fields = [1 => HoursField, 0 => MinutesField]; // 对应的取值范围是 [0 ~ 59, 0 ~ 23]
if (isset($parts[2]) && isset($parts[4])) {
$domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3));
$dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4));
$domExpression = new self($domExpression);
$dowExpression = new self($dowExpression);
$domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$combined = array_merge($domRunDates, $dowRunDates);
usort($combined, function ($a, $b) {
return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s');
});
return $combined[$nth];
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; ++$i) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (false === strpos($part, ',')) {
$satisfied = $field->isSatisfiedBy($nextRun, $part);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(0)->increment($nextRun, $invert, $parts[0] ?? null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
月、周、日、天、小时这几个字段都实现了 FieldInterface
接口
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp. (检查 DateTime 字段的相应值是否满足 CRON exp。)
*
* @param DateTimeInterface $date DateTime object to check
* @param string $value CRON expression to test against
*
* @return bool Returns TRUE if satisfied, FALSE otherwise
*/
public function isSatisfiedBy(DateTimeInterface $date, $value): bool;
/**
* When a CRON expression is not satisfied, this method is used to increment
* or decrement a DateTime object by the unit of the cron field. (当不满足 CRON 表达式时,此方法用于按 cron 字段的单位递增或递减 DateTime 对象。)
*
* @param DateTimeInterface $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
* @param string|null $parts (optional) Set parts to use
*
* @return FieldInterface
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface;
/**
* Validates a CRON expression for a given field.
*
* @param string $value CRON expression value to validate
*
* @return bool Returns TRUE if valid, FALSE otherwise
*/
public function validate(string $value): bool;
}
// 小时字段(HoursField)中实现的 increment 方法
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (null === $parts || '*' === $parts) {
$timezone = $date->getTimezone();
$date = $date->setTimezone(new DateTimeZone('UTC'));
$date = $date->modify(($invert ? '-' : '+') . '1 hour');
$date = $date->setTimezone($timezone);
$date = $date->setTime((int)$date->format('H'), $invert ? 59 : 0);
return $this;
}
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
$hours = [];
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = $date->format('H');
$position = $invert ? \count($hours) - 1 : 0;
$countHours = \count($hours);
if ($countHours > 1) {
for ($i = 0; $i < $countHours - 1; ++$i) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$hour = (int) $hours[$position];
if ((!$invert && (int) $date->format('H') >= $hour) || ($invert && (int) $date->format('H') <= $hour)) {
$date = $date->modify(($invert ? '-' : '+') . '1 day');
$date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
} else {
$date = $date->setTime($hour, $invert ? 59 : 0);
}
return $this;
}
getRunDate
方法中先用 $satisfied = $field->isSatisfiedBy($nextRun, $part);
检查 parts
中,对应位置上的值是否符合下一次任务执行的时间点,不满足时,使用 $field->increment($nextRun, $invert, $part);
对时间点进行调整
self::$order
中大的周期在上面,组装的 parts
中也是大的周期在最前面,所以后面处理的时候,会先处理大周期:
private static $order = [
self::YEAR,
self::MONTH,
self::DAY,
self::WEEKDAY,
self::HOUR,
self::MINUTE,
];
当前时间 2024-05-07 16:01:00
,定时任务的表达式是 29 14 * * *
,先处理大周期,所以第一个处理的周期是小时 14
当前时间中的小时是 16
,已经超过了 14
,HoursField
的 increment
方法中会加一天,同时将小时和分钟设置为 0,调整后的时间是:2024-05-08 00:00:00
getRunDate
方法中调用了increment
方法之后,会continue 2
,即:重新再验证一遍parts
是否符合;
第二次验证时,时间中的小时是 00
,小于 14
,即时间还未到,此时会将小时 14
设置到时间里,调整后的时间是:2024-05-08 14:00:00
小时验证通过后,第二个处理的周期是分钟29
,此时时间中的分钟是 00
,小于 29
,再将分钟 29
设置到时间里,调整后的时间是:2024-05-08 14:29:00
getRunDate
最终返回的下一次任务执行时间是 2024-05-08 14:29:00
,如果当前时间也走到了 2024-05-08 14:29:00
的时候,任务就会被触发。
标签:Laravel,return,currentTime,invert,Schedule,DateTime,dailyAt,parts,date From: https://www.cnblogs.com/zhpj/p/18178031/how-does-dailyat-work-in-laravel-schedule-work-zg1