JenkinsのCronはSafeRestart時にJob起動処理を落とさない
これは何
運用されているJenkinsにおいて、SafeRestart時にCronによるJobの発火をlostするのでは、といった懸念があった。 コードを読み、実際に動作確認をすると、再起動処理に2分以上かからなければat least onceでCronによるJobの発火がされることが分かった。
JenkinsをSchedulerとして利用する
JenkinsではJobの定義時にcron表記にてJobのscheduleを設定出来る。このcronは発火時刻になると、JobをJenkinsの処理Queueに入れる。
残念なことにJenkinsのHAはActive-Standbyのような構成しか取ることが出来ず、ActiveなJenkins masterのprocess内においてこれらのcronは処理される。もし冗長構成を取りたいのなら、Jenkinsが永続化として利用しているファイルストレージを、NFSのようなものを使うのが望ましい。
SchedulerがHAになっていないということは、再起動時等でJobの起動処理がロストするのでは、といった懸念がある。
SafeRestartの挙動
前提として、JenkinsのSafeRestartの挙動を確認する。SafeRestartは ${JENKINS_URL}/safeRestart
により提供されている、Jenkinsを安全に再起動する処理である。このSafeRestartを実行した際の挙動は以下のようになる。
- SafeRestartを実行
- 既に動いているJobがある場合には、そのJobが完了するまでは再起動を実行しない
- 新しくJobがQueueに入った場合には、そのJobはExecutorで処理されない
- 既に動いているPipelineがある場合
- 現在動いているPipelineのStepが完了するまでは再起動をしない
- 後続stepはQueueに入る
- 動いていたJobが完了すると、Queueの内容を
${JENKINS_HOME}/queue.xml
のファイルに永続化する - 再起動が完了すると、queue.xmlからQueueの状態を復元する
- 復元されたQueueからExecutorがJobを起動させる
以上が利用者目線でのざっくりとしたSafeRestartとJobの永続化処理/再起動処理となる。つまりSafeRestartにおいてJobやPipelineのStepがロストされることは基本的には無い。 その他にもagentとなるcomputerの停止やlistenerの処理を行っているが、ここでは割愛する。
Cronの挙動
次に確認するのは、Cronの挙動だ。仮に再起動時にprocessが一定時間動いていないのならば、その間に発火すべきCronがあったなら、そのJob起動処理をlostするのでは?といったことが懸念として考えられた。
例えばk8sのCronJobの場合には、前回いつ発火したのか
という情報を永続化することで、発火すべきタイミングで発火していないなら実行させるといった処理をさせている。
kubernetes/utils.go at master · kubernetes/kubernetes · GitHub
JenkinsのにはJobの履歴はあるものの、 前回いつ発火したのか
といった情報をCronでは使っていない。実際のCronのコードは以下のようになっている。
public static class Cron extends PeriodicWork { private final Calendar cal = new GregorianCalendar(); public Cron() { cal.set(Calendar.SECOND, 0); } public long getInitialDelay() { return MIN - TimeUnit.SECONDS.toMillis(Calendar.getInstance().get(Calendar.SECOND)); } public void doRun() { while(new Date().getTime() >= cal.getTimeInMillis()) { LOGGER.log(Level.FINE, "cron checking {0}", cal.getTime()); checkTriggers(cal); ... cal.add(Calendar.MINUTE,1); } } }
まず、Cronの起動チェック(doRun)は、毎分0秒に行われる。これは以下の理由によるもの。
- 起動時の
JOB_CONFIG_ADAPTED
処理後に、scheduleAtFixedRate
により1分ごとにdoRun
が呼び出されるようになる - 初回の呼び出し時には
getInitialDelay
待つ- 例えば13時10分40秒にJenkinsが起動した時には20秒待つ
- この時、初回の
Cron.doRun
の呼び出しは13時11分00秒になる
上記例の13時11分00秒の doRun
は、以下のように、 13時10分の checkTriggers と、13時11分の checkTriggers を呼び出す
- 13時10分40秒の起動時点で
cal
が13時10分00秒に初期化 - 13時11分00秒に
doRun
が呼ばれる- while の初回チェックでは、現在時刻13時11分00秒 >= cal 13時10分00秒により、
checkTriggers(13時10分00秒)
が呼び出される - while の2回目のチェックでは、現在時刻13時11分00秒 >= cal 13時11分00秒により、
checkTriggers(13時11分00秒)
が呼び出される - while の3回目のチェックでは、現在時刻13時11分00秒 >= cal 13時12分00秒により、loopから抜ける
- while の初回チェックでは、現在時刻13時11分00秒 >= cal 13時10分00秒により、
13時10分40秒に起動した場合には、13時11分00秒に、13時10分に発火すべきJobが起動するのである。
動作検証
実際に * * * * *
のように毎分起動するJobを定義して、SafeRestartをかけてみよう。Cronの挙動を確認するところで貼ったコードにある LOGGER.log(Level.FINE, "cron checking {0}", cal.getTime());
というログを見れば、どの時間帯にどの時刻のcalを用いた起動処理が走っているのかを確認出来る。
Jobを作り、以下のように ${JENKINS_URL}/log/
から、 hudson.triggers
のFINEログをJenkinsから閲覧出来るようにする。
よしなにSafeRestartをかけていくと、以下のような結果が得られた。
- 10:59:00に、
cron checking 2021/03/27 10:58
- 10:59:00に、
cron checking 2021/03/27 10:59
確かに 10時59分00秒に、10:58と10:59の起動処理が動いており、実際にその時間に2つのjobが動いている。
以上から、SafeRestart時に2分以上かからなければ、JenkinsのCronはSafeRestart時にJob起動処理を落とさないことがわかった。
2分以上かかっているかは、以下のInitMilestoneのログがINFOで出ている部分を確認すればよい。Jenkinsの WebUIが再起動後に使えない状態でも、jobがloadされていれば起動処理は行われる。
jenkins/InitMilestone.java at cea633560b7342957aef772e618eb9ba6ba89a87 · jenkinsci/jenkins · GitHub