Laravel Scheduler のコードリーディング



````
* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1

````

これがどうやって動いているかを考える。

## Schedule コマンド


コマンドの本体は 以下にある。

https://github.com/laravel/framework/blob/5.1/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php


````
    public function handle()
    {
        $eventsRan = false;

        foreach ($this->schedule->dueEvents($this->laravel) as $event) {
            if (! $event->filtersPass($this->laravel)) {
                continue;
            }

            $this->line('<info>Running scheduled command:</info> '.$event->getSummaryForDisplay());

            $event->run($this->laravel);

            $eventsRan = true;
        }

        if (! $eventsRan) {
            $this->info('No scheduled commands are ready to run.');
        }
    }
````

となっていて、`$this->schedule` は `\Illuminate\Console\Scheduling\Schedule` のよう。

### Schedule サービス

`\Illuminate\Console\Scheduling\Schedule` は、 `\App\Console\Kernel::schedule` にて引数に落ちてくるオブジェクト。 スケジュールの設定を行う際に触るオブジェクトである。

 https://github.com/laravel/framework/blob/5.1/src/Illuminate/Console/Scheduling/Schedule.php

 
   
foreach で参照されている、 `dueEvents` は以下のようになっている。
   
````
    public function dueEvents($app)
    {
        return collect($this->events)->filter->isDue($app);
    }
````

この`$this->events` は `\Illuminate\Console\Scheduling\Event`の配列。
`$this->events`は、`\App\Console\Kernel::schedule` 内で `$schedule->command("hoge:piyo")` の様に `command` メソドを実行した際に 追加されていく。

以下は、`\Illuminate\Console\Scheduling\Schedule::command` の中身。

````
    public function command($command, array $parameters = [])
    {
        if (class_exists($command)) {
            $command = Container::getInstance()->make($command)->getName();
        }

        return $this->exec(
            Application::formatCommandString($command), $parameters
        );
    }
    
    public function exec($command, array $parameters = [])
    {
        if (count($parameters)) {
            $command .= ' '.$this->compileParameters($parameters);
        }

        $this->events[] = $event = new Event($this->mutex, $command);

        return $event;
    }  
````

`$this->events` の正体がつかめたところで、`\Illuminate\Console\Scheduling\Schedule::dueEvents`の記述に戻る。
      
````
    public function dueEvents($app)
    {
        return collect($this->events)->filter->isDue($app);
    }
````

`collect` は `new Collection` なのだけども、ここで `Collection::$filter` は存在しない。

ややこしいけど色々マジック的な魔法を使っていて、上記の記述は以下とほぼ等価になる。

````
    public function dueEvents($app)
    {
        return collect($this->events)->filter(function(Event $event)use($app){
			return $event->isDue($app)        
        });
    }
````

絶対こっちの方が可読性は高いと思う。

## Event オブジェクト

次に Event オブジェクトを見ていく。 Event オブジェクトは `Schedule::command` から生成されリストに追加されていくもので、

そのリストのフィルタ関数内でコールされている `\Illuminate\Console\Scheduling\Event::isDue`は以下のような形になっている。

````
    public function isDue($app)
    {
        if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
            return false;
        }

        return $this->expressionPasses() &&
               $this->runsInEnvironment($app->environment());
    }
````

最初のセクションはメンテナンスチェック。常に false となる。

お目当ては、`\Illuminate\Console\Scheduling\Event::expressionPasses `。

`\Illuminate\Console\Scheduling\Event::runsInEnvironment` は発行される環境の制限。Kernel 側で以下のようにして実行環境を制限出来るよう。

````
$schedule->command('emails:send --force')->environment([...])
````

`\Illuminate\Console\Scheduling\Event::expressionPasses ` の方は以下のようになっている。

````
    protected function expressionPasses()
    {
        $date = Carbon::now();

        if ($this->timezone) {
            $date->setTimezone($this->timezone);
        }

        return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
    }
````

ここで `$this->expression` は 次の実行時刻の生成に用いられる。

`\Cron\CronExpression::isDue` は色々あるものの、最終的には、

````
    public function isDue($currentTime = 'now')
    {
    	// 割愛: $currentTime を timestamp 変換する処理

        try {
            return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
        } catch (Exception $e) {
            return false;
        }
    }
````

となるため、スケジューラは毎分実行しなければならない(前回実行したのがいつか…などを知る術はこの記述の中にはない)。


## 重複制御の仕組み

`withoutOverlapping` メソドに関しては、

````
    public function withoutOverlapping($expiresAt = 1440)
    {
        $this->withoutOverlapping = true;

        $this->expiresAt = $expiresAt;

        return $this->then(function () {
            $this->mutex->forget($this);
        })->skip(function () {
            return $this->mutex->exists($this);
        });
    }
    
    public function then(Closure $callback)
    {
        $this->afterCallbacks[] = $callback;

        return $this;
    }
    public function skip($callback)
    {
        $this->rejects[] = is_callable($callback) ? $callback : function () use ($callback) {
            return $callback;
        };

        return $this;
    }
````

`then`と `skip` の引数取扱の違いは謎だが、とりあえず上記の様になっている。

中でも `skip` で登録される、`reject` は `\Illuminate\Console\Scheduling\Event::filtersPass` の中で以下のように利用されている。

````
    public function filtersPass($app)
    {
        foreach ($this->filters as $callback) {
            if (! $app->call($callback)) {
                return false;
            }
        }

        foreach ($this->rejects as $callback) {
            if ($app->call($callback)) {
                return false;
            }
        }

        return true;
    }
````

`filterPass` は `Scheduler::handle` のイベントループ内で最初にコールされるため、`rejects` 内の `callable` がfalseを返せば、イベントはスキップされる仕組みになっている。

### Mutex

先程からちょくちょく挙がってくる `mutex` は `\Illuminate\Console\Scheduling\Mutex` で、`\Illuminate\Console\Scheduling\Schedule::__construct` にて登録されるものが、`$event`等にも Pass されている。



````
    public function __construct()
    {
        $container = Container::getInstance();

        $this->mutex = $container->bound(Mutex::class)
                                ? $container->make(Mutex::class)
                                : $container->make(CacheMutex::class);
    }
````

`Mutex` はタスクの実行記録等を行うサービスで、`Mutex`そのものはインターフェイスとなっている。サービスプロバイダ等で紐付けを行っていない限り、実装クラスの `CacheMutex` が用いられる。`CacheMutex` は Laravel の Cache を用いた Mutex 実装のため、ストレージの種別は Cache の設定に従う事となる。 expire の有効・無効もキャッシュの設定次第なので注意が必要


`\Illuminate\Console\Scheduling\Event::run`は以下のような実装になっており、実行前に mutex の登録が実施されることが伺える。

````
    public function run(Container $container)
    {
        if ($this->withoutOverlapping &&
            ! $this->mutex->create($this)) {
            return;
        }

        $this->runInBackground
                    ? $this->runCommandInBackground($container)
                    : $this->runCommandInForeground($container);
    }
    
    protected function runCommandInForeground(Container $container)
    {
        $this->callBeforeCallbacks($container);

        (new Process(
            $this->buildCommand(), base_path(), null, null, null
        ))->run();

        $this->callAfterCallbacks($container);
    }

````

## 結論

コードを見る限りでは

- 複数台サーバで CRON 実行しても、redis 等で mutex を共有すれば、重複制御は可能
- Heroku など 10分に1回の ツールでタタイても、`everyMinutes` のコマンドは5分に一回しか処理されない
- 逆に(複数台の運用も含めて)1分に1回 以上実行しても、その度に`everyMinutes`コマンドは実行される。

と言う感じのよう。

思っていること

- どっかでイベント取って、実行スケジュールをログ吐き出し出来ないか。
- then　で確実に forgetするのではなく、expire を調整する形で、永続化 mutex に載せた、next due time までの overlapping 制御は出来ないか。

どちらも overlaping 前提であれば、Mutex の実装で結構簡単にできそう。

前者は `Mutex::create` の実装で。

後者は `Mutex::forget` の実装で。 引数で落ちてくる $event は expression をもっている。

Task Hook という仕組みもあるよう。

https://laravel.com/docs/5.5/scheduling#task-hooks

面倒な時はもはや自分でイベントリストを作っても良いかも