かきノート

【Laravel】Schedule にて実行するコマンドは、withoutOverlapping() を付けないと、同じジョブが重複して実行される可能性がある

July 22, 2021 • ☕️ 7 min read

【 環境 】
Laravel のバージョン: 8.16.1
PHP のバージョン: 7.4.7
MySQL のバージョン: 5.7

Laravel のスケジューラについて

Laravel の Schedule には、withoutOverlapping() というメソッドがあり、これを使わない限り、タスクが重複して実行される事があるみたい。

【 公式サイト 】
https://laravel.com/docs/8.x/scheduling#preventing-task-overlaps

By default, scheduled tasks will be run even if the previous instance of the task is still running.
To prevent this, you may use the withoutOverlapping method:

つまり、「1分に1回実行するバッチ」があり、そのバッチの実行完了に 100秒かかるとすると、バッチの終了を待たずに次々と実行される、という挙動だろうか。

というわけで、実験してみる。


実験1:withoutOverlapping() を付けずに実行

1分に1回実行する、Batch01Command, Batch02Command, Batch03Command を設定。

app\Console\Kernel.php

    protected function schedule(Schedule $schedule)
    {
        $this->scheduleExecuteBatches($schedule);
    }

    private function scheduleExecuteBatches(Schedule $schedule)
    {
        $this->scheduleExecuteCommand01($schedule);
        $this->scheduleExecuteCommand02($schedule);
        $this->scheduleExecuteCommand03($schedule);
    }

    private function scheduleExecuteCommand01(Schedule $schedule)
    {
        $schedule->command(Batch01Command::class)
            ->everyMinute()
            ->runInBackground();
    }

    private function scheduleExecuteCommand02(Schedule $schedule)
    {
        $schedule->command(Batch02Command::class)
            ->everyMinute()
            ->runInBackground();
    }

    private function scheduleExecuteCommand03(Schedule $schedule)
    {
        $schedule->command(Batch03Command::class)
            ->everyMinute()
            ->runInBackground();
    }

Batch01Command.php

開始・終了にログを吐く。
100秒のウェイトをかけて、処理に 1分以上かかるようにする。

    public function handle()
    {
        $this->info(__METHOD__ . ':start');
        \Log::info(__METHOD__. ':start');

        // 100 秒間ウェイト
        sleep(100);

        $this->info(__METHOD__ . ':end');
        \Log::info(__METHOD__. ':end');
    }

    // Batch02Command, Batch03Command も、全く同じ処理

そして、以下のコマンドで待ち受けを起動。

php artisan schedule:work

以下、ログに出力された内容です。
今回の検証に不要な部分は、見づらかったのでカットしました。

storage\logs\laravel.log

[2021-07-21 12:51:07] Batch02Command::handle:start  
[2021-07-21 12:51:07] Batch01Command::handle:start  
[2021-07-21 12:51:09] Batch03Command::handle:start  
[2021-07-21 12:52:05] Batch01Command::handle:start  
[2021-07-21 12:52:05] Batch02Command::handle:start  
[2021-07-21 12:52:05] Batch03Command::handle:start  
[2021-07-21 12:52:47] Batch02Command::handle:end  
[2021-07-21 12:52:47] Batch01Command::handle:end  
[2021-07-21 12:52:49] Batch03Command::handle:end  
[2021-07-21 12:53:07] Batch02Command::handle:start  
[2021-07-21 12:53:07] Batch01Command::handle:start  
[2021-07-21 12:53:09] Batch03Command::handle:start  

(中略)

[2021-07-21 12:55:45] Batch01Command::handle:end  
[2021-07-21 12:55:45] Batch02Command::handle:end  
[2021-07-21 12:55:45] Batch03Command::handle:end  
[2021-07-21 12:56:05] Batch01Command::handle:start  
[2021-07-21 12:56:05] Batch02Command::handle:start  
[2021-07-21 12:56:06] Batch03Command::handle:start  
[2021-07-21 12:56:48] Batch02Command::handle:end  
[2021-07-21 12:56:48] Batch01Command::handle:end  
[2021-07-21 12:56:48] Batch03Command::handle:end  
[2021-07-21 12:56:56] Batch01Command::handle:end  
[2021-07-21 12:56:56] Batch02Command::handle:end  
[2021-07-21 12:56:56] Batch03Command::handle:end  

Batch01Command, Batch02Command, Batch03Command は、それぞれのジョブの終了を待たず、同じジョブが同時に走っている。

業種や処理内容によっては、調査が非常に難しい障害が発生しそうなので、この挙動は抑えておいたほうがよさそう。


実験2:withoutOverlapping() を付けて事項

各バッチを「withoutOverlapping()」を付けて実行。
それ以外は「実験1」と全く同じ。

app\Console\Kernel.php

    private function scheduleExecuteCommand01(Schedule $schedule)
    {
        $schedule->command(Batch01Command::class)
            ->everyMinute()
            ->runInBackground()
            ->withoutOverlapping();
    }

    private function scheduleExecuteCommand02(Schedule $schedule)
    {
        $schedule->command(Batch02Command::class)
            ->everyMinute()
            ->runInBackground()
            ->withoutOverlapping();
    }

    private function scheduleExecuteCommand03(Schedule $schedule)
    {
        $schedule->command(Batch03Command::class)
            ->everyMinute()
            ->runInBackground()
            ->withoutOverlapping();
    }

以下、ログに出力された内容です。

storage\logs\laravel.log

[2021-07-21 13:09:07] Batch02Command::handle:start  
[2021-07-21 13:09:07] Batch01Command::handle:start  
[2021-07-21 13:09:09] Batch03Command::handle:start  
[2021-07-21 13:10:47] Batch02Command::handle:end  
[2021-07-21 13:10:47] Batch01Command::handle:end  
[2021-07-21 13:10:49] Batch03Command::handle:end  
[2021-07-21 13:11:08] Batch01Command::handle:start  
[2021-07-21 13:11:08] Batch02Command::handle:start  
[2021-07-21 13:11:10] Batch03Command::handle:start  
[2021-07-21 13:12:48] Batch01Command::handle:end  
[2021-07-21 13:12:48] Batch02Command::handle:end  
[2021-07-21 13:12:50] Batch03Command::handle:end  
[2021-07-21 13:13:08] Batch01Command::handle:start  
[2021-07-21 13:13:08] Batch02Command::handle:start  
[2021-07-21 13:13:09] Batch03Command::handle:start  
[2021-07-21 13:14:48] Batch02Command::handle:end  
[2021-07-21 13:14:48] Batch01Command::handle:end  
[2021-07-21 13:14:49] Batch03Command::handle:end  

さっきとは違い、重複して同じバッチが実行されないようになっている。

業務アプリなら、もう無条件で付けといてよいのではないだろうか。
余計な事故に発展しそうだし。

1分に1回動くスケジューラが動いているのですが、前回起動のジョブが完了しておらず、実行できるジョブが無かった場合、コンソールからはこんなメッセージが表示されました。

No scheduled commands are ready to run.

補足

こんなのがありました。

【Laravel】 Cron タスクスケジューラの onOneServer() と withoutOverlapping() の違い

withoutOverlapping() は onOneServer() と併用して,初めて意味のある選択肢になります。

えええ!!! そうなの!?

と思いきや、
「重複起動しないように withoutOverlapping() を付けるんだろうけど、onOneServer() も付けないと他のサーバで重複実行される可能性があるから、併用しないと片手落ちになっちゃうよ。」
っていう意味か。

初見、
「withoutOverlapping() は、onOneServer() と一緒に使わないと意味をなさない(重複起動を防ぐ、という機能を満たさない)」
という意味かと思ってしまった。
(もちろん、そんな事は無い事は上記で検証済み)

という事で、バッチを実行するサーバが1台のみの場合、「withoutOverlapping()」だけでも、ジョブの重複起動を防止できます。

まぁ、そのうちサーバが増えた時に「onOneServer()」を追加するとか面倒なので、無条件で「onOneServer()」をくっつけといた方がいんじゃないかと思います。

理由としては、サーバ1台で動かす事を前提とした場合でも、特に何の問題も無いと確信が持てたからです。

調査内容が、こちら。
【Laravel】Schedule の onOneServer って、どんな動きをしているの? Laravel のソースコードを追ってみた。

結論

Schedule にてジョブを起動している場合、重複して実行させたくない場合、
「->withoutOverlapping()」を付けよう!


Relative Posts:

【Laravel】Schedule クラスから command を実行する時、onSuccess と onFailure でエラーを検知しよう

July 23, 2021

【Laravel】schedule からコールされる command は、後続のメソッド次第で同期的に処理されたり非同期で処理されたりする(runInBackground)

July 21, 2021

福岡の物流エンジニアが、七転び八起きしたあと九回転び、寝っ転がったまま何かやってる事を垂れ流しているブログ

RotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-icon