細かすぎて伝わらない anacron の挙動

対象は CentOS 6 に付属の cronie-anacron-1.4.4。

## 開始時間についての問題

`/etc/anacrontab` がRHEL 6/CentOS 6系のデフォルトのまま以下のようになっていて、

```sh:/etc/anacrontab
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22

#period in days   delay in minutes   job-identifier   command
1	5	cron.daily		nice run-parts /etc/cron.daily
7	25	cron.weekly		nice run-parts /etc/cron.weekly
@monthly 45	cron.monthly		nice run-parts /etc/cron.monthly
```

anacron 自体の起動も、デフォルトのまま `/etc/cron.d/0hourly` と `/etc/cron.hourly/0anacron` によって毎時1分に起動されているとする。

このとき、上記の `cron.weekly` ジョブの開始時間の範囲は、何時何分から何時何分までになるか？

## 誤った回答

まず遅延時間を考えると、

* デフォルトの最短遅延が6分、最長遅延は上記の `RANDOM_DELAY` で45分
* `cron.weekly` の固定の遅延が25分
* あわせると、`cron.weekly` の取り得る遅延は31分〜70分

で、

* ジョブが実行される時間の範囲は、`START_HOURS_RANGE` により3時から22時の間
* anacron 自体は、毎時1分に起動する

ことから、ジョブが実行できる状況なら 3:32〜4:11 の間のどこかで起動する……

というのは**誤り**である。

## 正しい回答

anacron のソースで `START_HOURS_RANGE` のチェックを行っている箇所を見てみると、以下のようになっている。

```c:main.c
time_t jobtime = start_sec + job_array[j]->delay * 60;

t = localtime(&jobtime);
if (range_start != -1 && range_stop != -1 &&
    (t->tm_hour < range_start || t->tm_hour >= range_stop))
{
    /* ジョブをスキップする処理 */
```

この `if` にマッチした場合 `START_HOURS_RANGE` の範囲外としてジョブがスキップされるが、この基準となる時間は anacron の起動時間（ソース中の `start_sec`）ではなく、それにジョブごとの遅延時間（ソース中の `delay`）を加えた時間になっている。定義からすると当然のことで、遅延も考慮に入れた上で、ジョブが実際に動作する時間で判定しているわけだ。

なので、最初の問題の場合、2:01 の anacron の実行時に遅延が59分以上になったジョブは開始時間が 3:00 以降になるため、スキップはされず、そのまま 2:01 の回の anacron によって実行される。この場合は 3:00〜3:11 のどこかで起動することになる。

つまり、この設定での `cron.weekly` のジョブが起動する時刻は、

* 3:00〜3:11 （2:01の回の anacron で起動された場合）、または
* 3:32〜4:11 （3:01の回の anacron で起動された場合）

のどこか、というのが正解になる。

## 派生する問題

デフォルトの設定の場合、`cron.daily` ジョブの遅延時間は11分〜50分なので、2:01 の回の anacron で起動されることはなく、必ず 3:01 の回で起動する（3:12〜3:51 の間）。

しかし、anacron の動作として

1. `/var/spool/anacron/` 以下にあるジョブ名と同じ名前のファイルを読み、その中に書かれている前回実行日付から、ジョブを実行対象とするかどうかを判定する。たとえば週次ジョブなら、 `/var/spool/anacron/cron.weekly` ファイルに記載の前回実行日から7日経過しているかを判定する。
2. 所定の日数が経過していて実行対象と判定されたジョブについて、この前回実行日ファイルをロックする。
3. この後で、前述の `START_HOURS_RANGE` のチェックを行う。実行対象外と判定されたジョブについても、anacron 自体の完了まで前回実行日ファイルはアンロックされない。

という順序になっているため、前述のように 2:01 の回の anacron が 3:01 以降まで `cron.weekly` の起動待ちになっている場合、実際には起動されない `cron.daily` の前回実行日ファイルも一緒にロックされたままになっている。

この状態で 3:01 の回の anacron が `cron.daily` を起動しようとすると、上の 2. のロックに失敗し、ログに

```
Job 'cron.daily' locked by another anacron - skipping
```

と出力して起動をスキップしてしまう。結果、`cron.daily` ジョブは次回の起動タイミング 4:12〜4:51 まで起動が遅れることになる。

anacron で日次ジョブを実行しているが、なぜか特定の曜日だけたまに起動時間が1時間くらい遅くなる……のようなことがあったら、おそらくこういう挙動が原因と思われる。

## その他

anacron の起動メカニズムや挙動については、以下がわかりやすい。

https://www.reqtc.com/kowaza/anacron1.html

ただ、

> 「遅延時間」＝共通設定にてランダム決定した遅延時間＋
>　　　　　　　　　ジョブ個別設定にてランダム決定した遅延時間

とあるが、ジョブ個別設定の方の遅延時間は、ランダムではなくそのまま加算だと思われる。

あと、Red Hatの公式ドキュメント等には

https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html

> RANDOM_DELAY — ジョブごとに指定されている delay in minutes 変数に追加される最大の時間分数。
> 最短の遅延時間は、デフォルトで 6 分に設定されています。

とあるが、6分に調整するロジックがソースのどこにも見つからない。ソース上は、

```c:readtab.c
int i = random();
double x = 0;
x = (double) i / (double) RAND_MAX * (double) (atoi(value));
random_number = (int)x;
Debug(("Randomized delay set: %d", random_number));
```

のように 0 から `RANDOM_DELAY` までの値をとるようになっていて（`value` は `RANDOM_DELAY` の設定値）、これ以降も何か調整している形跡がない。最低6分なら、デフォルト設定では `cron.daily` の遅延は必ず11分以上になるはずだが、手元の cron ログでは

```
/var/log/cron:Feb  9 03:01:01 myhost anacron[25024]: Will run job `cron.daily' in 9 min.
```

というのがあるので、マニュアルの方が間違っているんじゃないかと思う。