テストは通っているのに、定期ジョブが一度も動いていなかった話

当ページのリンクには広告が含まれています。
この記事の結論
定期ジョブの次回実行時刻は、プロセスの中ではなくプロセスの外に置く必要があると考えた話です。起動を基準にして次回を決めていると、デプロイで再起動が続く環境では起点が毎回リセットされ、ジョブ本体のテストが通っていても実運用では一度も呼ばれません。

お疲れ様です!IT業界で働くアライグマです!

個人開発している開発関連ニュースのキュレーションサービス「DevPick」で、定期ジョブの実行タイミングの持ち方を見直しました。24時間ごとの掃除処理と3時間ごとの画像再取得が、実運用では一度も動いていない条件を満たしていたためです。原因はジョブの中身ではなく、次にいつ動くかを決める側にありました。

目次

90日で消えるはずの記事が、消えていなかった

DevPickには、時間が経った記事を片付ける処理があります。保持期間として90日を決めていて、それを過ぎても「いいね」が付いていない記事と、その記事にぶら下がる関連レコードをまとめて削除します。放っておくとデータベースが増え続けるので、24時間に1回動かす想定でした。

同じ枠組みで、記事のサムネイル画像を後から取り直す処理も動かしていました。記事を取り込んだ時点でリンク先から画像情報を取れなかった場合に、3時間ごとに対象を拾い直して再取得します。この再取得には7日という期限を設けていて、期限を過ぎたものは対象から外れます。

問題は、この2つが動いた形跡を確認できなかったことです。保持期間を過ぎた記事が残り、送信を終えた通知の待ち行列や期限切れの復旧コードも片付いていませんでした。画像のほうも、一時的な失敗から立ち直る機会を得られないまま7日の期限を越えれば、画像無しの状態がそのまま固定されます。

厄介だったのは、ジョブの中身にはまったく問題が無かった点です。削除処理も再取得処理もテストは通っていました。テストが確認していたのは「呼ばれたら正しく動くこと」であり、「実運用で呼ばれていること」ではありません。以前にバックアップが取れていても戻せるかは別だった話を書きましたが、今回も似た構図でした。用意した仕組みが存在することと、それが実際に働いていることは別々に確認する必要があります。

IT女子 アラ美
中身のテストが緑でも、そもそも呼ばれてなかったら意味ないもんね。

ITアライグマ
はい。動くことと動いていることは、別々に確かめる必要がありました。

「起動から24時間後」はデプロイのたびに振り出しへ戻る

原因は、次にいつ実行するかの決め方にありました。旧実装では、掃除処理を24時間の間隔、画像の再取得を3時間の間隔で登録していましたが、初回にいつ動かすかを明示していませんでした。この場合、最初の実行はプロセスが起動してから1間隔後になります。つまり「起動から24時間後」「起動から3時間後」です。

一見すると自然な設定に見えます。ところがDevPickのバックエンドは、mainへマージするたびに自動デプロイが走り、その中でサービスが再起動します。再起動すればプロセスは作り直されるので、次回実行の起点も新しい起動時刻へ書き換わります。

ここで、デプロイの間隔がジョブの間隔より短いと何が起きるか。24時間経つ前に再起動が入り、次回実行は「新しい起動から24時間後」へ先送りされます。その24時間が経つ前にまた再起動が入れば、また先送りです。デプロイを続けている限り、この先送りが終わりません。実際に2026年8月28日から30日にかけてのmainの履歴は24時間未満の間隔でデプロイが続いており、この条件をきれいに満たしていました。

言い換えると、次回実行の予定をプロセスの内側だけに持っていたことが問題でした。プロセスが消えれば予定も一緒に消えるので、何度再起動しても常に「これから1間隔」の状態から始まります。時間は前に進んでいるのに、期限だけが同じ距離を保って逃げていく形です。

IT女子 アラ美
デプロイするほど実行が遠ざかるって、頑張るほど損する仕組みじゃない。

ITアライグマ
まさにそこが盲点でした。開発を止めれば動く、という妙な条件でした。

同じファイルの中に、飢餓を避けていたジョブもあった

調べていて意外だったのは、同じ場所に登録されている他のジョブは、この問題を踏んでいなかったことです。記事の取得、ダイジェスト通知の送信、復旧メールの送信は、いずれも起動した直後に1回実行する形で登録していました。

起動直後に実行する設定にしてあれば、再起動が何度入っても、そのたびに1回は動きます。先送りが積み重なりません。これらのジョブは「なるべく早く動いてほしい」という理由でその形にしていたのですが、結果として再起動による飢餓、つまり順番が回ってこないまま処理されない状態を避けられていました。

一方、掃除処理と画像の再取得は、性質上そこまで急がない処理です。だからこそ初回を1間隔後に任せていたのですが、その判断がそのまま「再起動のたびに先送りされる」という副作用につながっていました。急がなくてよいことと、いつまでも動かなくてよいことは、まったく別の話です。

同じファイルの中に、同じ問題に対する答えを持つジョブと持たないジョブが並んでいたわけです。ジョブを1つ追加するとき、他のジョブがどんな条件を前提にしているかまでは見ていませんでした。個別には正しい判断でも、揃っていないと、揃っていない側だけが静かに落ちます。

IT女子 アラ美
隣のジョブが正解を持ってたのに、そこだけ抜けてたのはちょっと悔しいわね。

ITアライグマ
はい。同じ場所に並ぶ設定こそ、揃っているかを見る必要がありました。

次に実行する時刻をプロセスの外へ置く

対応の中心は、次に実行する時刻を「起動からの相対時間」ではなく「絶対的な時刻」として決め、それをプロセスの外へ保存することでした。プロセスが作り直されても、保存した時刻は残ります。

起動時の流れは次のようになります。まず保存済みの次回実行時刻を読みます。それがまだ未来なら、その時刻をそのまま使います。ここで新しく計算し直さないことが重要で、再起動しても予定が動きません。保存が無い初回や、すでに期限を過ぎている場合だけ、短い起動猶予を足した時刻を計算します。

そして計算した時刻は、実行を待つ前に保存します。処理の順序を単純化すると、次のような形です。

保存済みの次回実行時刻を読む
  └ 未来の時刻がある → その時刻で予約する(再計算しない)
  └ 無い / 過ぎている → 今 + 起動猶予 を計算し、保存してから予約する

猶予を足した時刻を先に保存しておくと、その猶予の最中に再起動が入っても、次の起動では「未来の保存済み時刻」を読む側の分岐に入ります。猶予がさらに猶予を生むことがありません。旧実装が抱えていた先送りの連鎖は、ここで止まります。

猶予の長さは処理ごとに変えました。掃除処理は1分で、初回から24時間待たずに動きます。画像の再取得は10分にしています。起動直後には記事の取得が走るので、同じタイミングで外部サイトへリクエストを重ねたくなかったためです。この再取得はサムネイル取得に検証と再取得の上限を足した話で書いた仕組みで、対象と回数を絞ってあります。それでも、相手先への配慮として起動直後の集中は避けたいところでした。

IT女子 アラ美
待つ前に予定を書いておくのって、地味だけどすごく効きそうな順番ね。

ITアライグマ
はい。待ってから保存すると、待っている間の再起動で消えてしまいます。

期限はジョブ本体より先に進める

もう1つ決めたのが、次回の期限を進めるタイミングです。ジョブが動き出したとき、処理本体を呼ぶ前に期限を先へ進めるようにしました。

処理を終えてから進めるほうが、素直な順序に見えます。ただ、掃除処理は対象が多ければ時間が掛かりますし、その最中にデプロイが入って再起動することもあります。処理後に進める設計だと、途中で落ちた場合に期限が過去のまま残り、次の起動で同じ期限をもう一度実行してしまいます。DevPickはプロセスを複数動かしているので、担当が切り替わったときにも同じことが起こり得ます。

先に進めておけば、期限は常に未来を指します。同じ期限を二重に処理することはありません。掃除も画像の再取得も、1回飛ばしたところで次の周期で拾えます。二重実行のほうが実害が大きい処理なので、この順序を選びました。

進め方にも一手間かけています。何らかの理由で実行が大きく遅れ、間隔をいくつも跨いでしまった場合、過去の期限を1回ずつ再生すると、溜まった分だけ連続で実行されてしまいます。そうではなく、現在時刻より未来になるまで期限をまとめて進める形にしました。普段は1回足すだけで終わりますが、遅れた分を後追いで一気に処理する事故を防げます。合わせて、取りこぼしを猶予つきで救う設定は外し、溜まった発火はまとめて1回として扱い、同時に走るのは常に1本までとしています。

IT女子 アラ美
遅れた分を全部やり直すより、今から仕切り直すほうが安全なのね。

ITアライグマ
はい。掃除も画像の再取得も、遅れを取り戻す必要のない処理でした。

状態を読めないときは、止めるより動かす側へ倒す

次回実行時刻をプロセスの外へ出したことで、新しい心配事も増えました。その保存先を読み書きできなかったとき、どう振る舞うかです。今回の変更は「定期ジョブが動かない」問題を直すためのものなので、その保存先の不調が新しい停止理由になっては本末転倒です。

そこで、失敗したときは動かす側へ倒すと決めました。起動時に保存済みの時刻を読めなかった場合は、起動を止めず、猶予を足した近い時刻をそのまま使って予約します。実行時に期限を進められなかった場合も、記録の更新失敗を理由に処理本体を止めず、そのまま実行します。

この選択は、重複と停止を天秤にかけた結果です。期限を進められないまま実行すれば、同じ期限がもう一度実行される可能性があります。一方、失敗を理由に実行を見送れば、元の「動かない」状態へ戻ります。掃除も画像の再取得も、二重に走ってもデータが壊れる性質ではないため、無期限に止まるほうを避けました。

テストもこの判断ごとに用意しました。猶予の最中に何度再起動しても最初に予約した時刻を保つこと、期限超過時の猶予も保存して繰り返し先送りしないこと、状態を読めなくても起動を止めないこと、遅れても過去分を再生せず元の周期上の未来へ進むこと、記録の更新に失敗しても処理本体が実行されること。「呼ばれたら正しく動くか」ではなく「どんな条件でも呼ばれるか」を確かめる観点です。最初に取りこぼしたのがまさにそこだったので、そこを固定しました。

IT女子 アラ美
失敗したときにどっちへ転ぶかまで、先に決めておくのが大事なのね。

ITアライグマ
はい。何を守り、何を諦めるかを決めないと、安全側が選べません。

まとめ

DevPickの掃除処理と画像の再取得は、次回実行の時刻をプロセスの起動基準で決めていました。そのため、mainへのマージごとに再起動が入る運用と噛み合わず、間隔より短い頻度でデプロイが続く限り、いつまでも実行されない状態になっていました。ジョブ本体のテストは通っていたので、動いていないことに気づく手がかりもありませんでした。

対応として、次に実行する絶対時刻をプロセスの外へ保存し、起動時に未来の時刻があればそれをそのまま使う形にしました。猶予を足した時刻も待つ前に保存し、実行時は処理本体より先に期限を進めます。保存先を読み書きできない場合は、止めるより動かす側へ倒しています。

今の私の結論は、定期実行の設計では処理の中身と同じくらい、次にいつ動くかを誰が覚えているかが重要だということです。プロセスの中だけで覚えている予定は、そのプロセスが消えれば一緒に消えます。デプロイの頻度が上がるほど、その前提は静かに崩れていきます。

IT女子 アラ美
動かない仕組みって、壊れた仕組みよりも見つけにくいのが怖いところね。

ITアライグマ
はい。エラーが出ない不具合ほど、気づく仕掛けを先に持ちたいところです。

作者が開発したサービス「DevPick」

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次