记录解决 $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');

但是上面两种方法都有一个缺陷,就是只能运行一次,不能多进程多服务器运行,都是利用锁的机制去控制时间段内一天一次

大家有好的解决方案可以发出来参考参考

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 5
Epona

为什么不直接设置到每天6:00执行任务呢

4年前 评论

@Epona 是可以的,只是越早执行越好

4年前 评论
Epona

@ab0029 所以你就给自己增加了难度,我的话就直接设置到6点了😂,另外我觉得应该是这个逻辑,在1--6点之间每小时执行一次,在这个任务的代码中判断是否已执行,这样就不需要你魔改代码了。

4年前 评论

@Epona 1-6每小时执行一次也可以,但是业务就必须加入不必要的代码去控制这个时间段,放到外面去更好点,各有利弊,只是刚好遇到这个问题想去解决,粗暴一点的就是像你说得,在6点执行,或者某个不会维护的时间点执行就可以了。也不算牛角尖,毕竟我这边生成环境维护时间通常在凌晨后维护,确实需要每天的时间段内执行就可以解决问题。

4年前 评论
Epona 4年前

@Epona 突然想到6.0后可以使用任务中间件了,可以添加中间件然后配合每小时执行一次去解决

4年前 评论
Epona 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!