记录解决 $schedule->daily ()->between ('x','x') 不执行的问题
今天做任务调度时需要每天调度一次任务,所以就直接用框架自带的任务调度,一开始还是很顺利的
在App\Console\Kernel
添加以下代码
$schedule->job(new DailyJob($time))->daily();
运行起来很完美,每天凌晨的时候就执行了
可是发现有时候系统凌晨时候在维护期,所以调整了下触发时间在1点
$schedule->job(new DailyJob($time))->dailyAt('1:00');
运行起来依然是完美的,1点的时候就执行了
后面又发现维护期可能1点还没维护好,这就有点尴尬了,所以想调整在1点到6点之间触发就可以了
$schedule->job(new DailyJob($time))->daily()->between('1:00', '6:00');
结果问题来了,发现这个调度一直没有运行,无论是在凌晨还是1:00-6:00之间都是没有执行到,看下源码后,发现daily设置的表达式为0 0 * * *
框架执行调度判断核心代码如下:
/**
* 判断是否匹配 维护期,时间,运行环境
*/
public function isDue($app)
{
if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
return false;
}
return $this->expressionPasses() &&
$this->runsInEnvironment($app->environment());
}
/**
* 这个方法,直接把daily()过滤掉了,只有在凌晨时候才能触发
*/
protected function expressionPasses()
{
$date = Carbon::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
}
如果刚好在凌晨,应该是可以促发任务的,但是后面我们带了between('1:00', '6:00')
,between
是生成一个过滤函数
/**
* 生成一个和当前时间判断的过滤函数
*/
public function between($startTime, $endTime)
{
return $this->when($this->inTimeInterval($startTime, $endTime));
}
凌晨这个时候daily()
是通过的,但是between()
的过滤是不通过的,所以任务就一直不执行,当到了1点,两个条件也是不能同时满足,所以就废掉了,一个永远不会调度的任务产生了
研究了下框架的源码,发现还是有办法解决的,列出两种方案:
一,直接通过过滤函数实现
// 每天区间任务另外种实现
// 因为我用的是job,所以event是一个Illuminate\Console\Scheduling\CallbackEvent
// 计算距离今天结束还有多少分钟
$diffMinutes = now()->endOfDay()->diffInMinutes(now());
// 因为我们不用daliy()去实现,所以withoutOverlapping必须要有
$event = $schedule->job(new DailyJob())->between('1:00', '6:00')->withoutOverlapping($diffMinutes);
$event->then(function() use ($event) {
// 任务执行完成把withoutOverlapping设置为false,因为CallbackEvent会在程序销毁时候调用解锁,当withoutOverlapping为false的时候触发不了解锁
$event->withoutOverlapping = false;
// 锁住任务,下次进来就因为锁过滤掉了,不会运行这个调度
$event->mutex->create($event);
});
二,通过宏实现(个人偏向这个方法)
// 在App\Providers\AppServiceProvider boot方法里面加入
CallbackEvent::macro('dailyBetween', function($startTime, $endTime) {
$this->between($startTime, $endTime);
// 自定义一个锁名称
$lockKey = $this->mutexName() . '-' . sha1($startTime. $endTime);
// 计算到今天结束还有多少秒
$diffSeconds = now()->endOfDay()->diffInSeconds(now());
// 生成锁类
$lock = \Cache::lock($lockKey, $diffSeconds);
$this->then(function() use ($lock) {
// 任务处理完获取锁
$lock->get();
})->skip(function () use ($lock) {
// 能获取锁说明能运行
$result = !$lock->get();
if ( !$result) {
// 把锁释放,任务处理完才锁住
$lock->release();
}
return $result;
});
});
// 然后调用dailyBetween即可了
$schedule->job(new DailyJob())->dailyBetween('1:00', '12:00');
但是上面两种方法都有一个缺陷,就是只能运行一次,不能多进程多服务器运行,都是利用锁的机制去控制时间段内一天一次
大家有好的解决方案可以发出来参考参考
为什么不直接设置到每天6:00执行任务呢
@Epona 是可以的,只是越早执行越好
@ab0029 所以你就给自己增加了难度,我的话就直接设置到6点了😂,另外我觉得应该是这个逻辑,在1--6点之间每小时执行一次,在这个任务的代码中判断是否已执行,这样就不需要你魔改代码了。
@Epona 1-6每小时执行一次也可以,但是业务就必须加入不必要的代码去控制这个时间段,放到外面去更好点,各有利弊,只是刚好遇到这个问题想去解决,粗暴一点的就是像你说得,在6点执行,或者某个不会维护的时间点执行就可以了。也不算牛角尖,毕竟我这边生成环境维护时间通常在凌晨后维护,确实需要每天的时间段内执行就可以解决问题。
@Epona 突然想到6.0后可以使用任务中间件了,可以添加中间件然后配合每小时执行一次去解决