概要
毎週、前の1週間に投稿したつぶやきを1本のまとめ記事にして、はてなブログへ下書きとして自動投稿する機能を運用している。投稿が終わると「週間まとめを下書き投稿しました」と Slack に通知が飛ぶ仕組みだ。
ところが、本来は「毎週月曜の朝に、月曜〜日曜の1週間分」を投稿してほしいのに、実際には毎週日曜に発火し、期間も「日曜〜土曜」とずれて投稿されていた。この記事では、その原因を2つ突き止めて直した過程を記録する。
どんな機能か
まず全体像を整理しておく。週間まとめ投稿は、役割の違う2つの部品でできている。
- 時計の役割(定期実行): Cloudflare Workers の Cron Trigger。決まった曜日・時刻になると、後述のエンドポイントを1回だけ叩く中継役。
- 本体の役割(記事生成と投稿): Cloudflare Pages 側のサーバー関数。前週分の投稿をデータベースから取り出し、本文に使われている画像をはてなフォトライフへ移し替えたうえで、はてなブログへ下書きを投稿し、結果を Slack に通知する。
時計が本体を起こし、本体が記事を組み立てて投稿する、という分担だ。
症状
Slack には毎週日曜に、次のような通知が届いていた。
週間まとめを下書き投稿しました
期間: 5月24日(日)〜5月30日(土)
期待していたのは「月曜の朝に、5月25日(月)〜5月31日(日)」という通知だ。曜日が1日前にずれ、しかも週の区切りが「日〜土」になってしまっている。
原因その1:Cron の曜日指定が間違っていた
定期実行の設定ファイルには、こう書かれていた。
# Cloudflare Workers は 0=日曜, 1=月曜 ではなく 1=日曜, 2=月曜 と解釈するため 2 を指定
crons = ["0 0 * * 2"]
このコメントが事実誤認だった。Cron 式の5つ目の数字は曜日を表すが、Cloudflare Workers の Cron は特別な数え方をするわけではなく、一般的な cron の仕様に従う。つまり次のとおりだ。
| 数字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 曜日 | 日 | 月 | 火 | 水 | 木 | 金 | 土 |
「2 = 月曜」だと思い込んで 2 を指定していたが、本当は 2 は火曜だ。つまり設定上は火曜に発火するはずで、いずれにせよ意図した月曜にはなっていなかった。
正しくは月曜を表す 1 を指定する。時刻の 0 0 は「0時0分(UTC)」の意味で、日本時間(JST)は UTC より9時間進んでいるので、これは月曜の朝9時(JST)にあたる。
# Cloudflare Workers の Cron は標準仕様(0=日曜, 1=月曜, ... 6=土曜)。月曜は 1 を指定する。
crons = ["0 0 * * 1"]
原因その2:期間の計算が「発火日から7日さかのぼるだけ」だった
曜日の指定を直しても、もう1つ問題が残っていた。投稿対象の期間を決める処理が、こうなっていたのだ。
// 発火した瞬間の日本時間
const nowJstMs = Date.now() + 9 * 60 * 60 * 1000;
// 「前日」から「7日前」までを対象にする
const startDate = toDateStr(nowJstMs - 7 * MS_PER_DAY);
const endDate = toDateStr(nowJstMs - 1 * MS_PER_DAY);
これは「動いた日を基準に、ただ7日さかのぼる」だけの計算だ。月曜に動く前提なら、前日は日曜、7日前は前の月曜になり、ちょうど「月〜日」になる。だが曜日に固定(アンカー)していないので、発火する曜日がずれると、期間ごと丸ごとずれてしまう。
実際、日曜に発火していたために「前日 = 土曜」「7日前 = 日曜」となり、症状どおり「日〜土」になっていた。原因その1とこの設計が組み合わさって、ずれた期間で投稿され続けていたわけだ。
直し方:曜日を基準に「先週の月〜日」を固定する
そこで、発火した曜日が多少ずれても、必ず「直近で完了した月〜日の1週間」を対象にするよう作り替えた。
const nowJstMs = Date.now() + 9 * 60 * 60 * 1000;
// 日本時間での今日の曜日(0=日, 1=月, ... 6=土)
const jstWeekday = new Date(nowJstMs).getUTCDay();
// 直近で完了した「日曜」までの日数。日曜に動いた場合は当日ではなく1週間前を指す
const daysToLastSunday = jstWeekday === 0 ? 7 : jstWeekday;
// 先週の日曜(週の終わり)と、その6日前の月曜(週の始まり)
const endDate = toDateStr(nowJstMs - daysToLastSunday * MS_PER_DAY);
const startDate = toDateStr(nowJstMs - (daysToLastSunday + 6) * MS_PER_DAY);
ポイントは daysToLastSunday(直近の日曜まで何日さかのぼるか)だ。曜日の数字がそのまま「直近の日曜までの日数」になる。たとえば月曜(1)なら1日前が日曜、火曜(2)なら2日前が日曜だ。ただし日曜(0)のときだけは、まだ終わっていない今週を拾わないよう、あえて7日前(先週の日曜)を指すようにしている。
こうして求めた日曜を週の終わりとし、その6日前を週の始まり(月曜)とすれば、いつ動いても「先週の月〜日」に必ずそろう。
月曜の朝に動いた場合を計算で確かめると、対象期間は 5月25日(月)〜5月31日(日) となり、期待どおりになった。
デプロイで気づいたこと:設定変更は再デプロイが必要
設定ファイルを直しても、それだけでは本番の動きは変わらない。Cloudflare Workers の Cron は、デプロイした時点の設定が反映される。修正したファイルから改めてデプロイし直して、はじめて新しいスケジュールが有効になる。
デプロイ後の出力で、スケジュールが切り替わったことを確認した。
Deployed hatena-weekly-cron triggers
schedule: 0 0 * * 1
末尾が 1(月曜)になっていれば反映完了だ。最初は修正の入っていない方の作業ディレクトリから誤ってデプロイしてしまい、0 0 * * 2(火曜)のまま上書きされてしまった。「どの場所のファイルをデプロイしているか」を取り違えないよう注意が必要だと痛感した。
学び
- Cloudflare Workers の Cron の曜日は、一般的な cron と同じ「0=日曜」始まり。独自のずれた数え方はしない。思い込みでオフセットを足さないこと。
- 定期実行する処理の「期間計算」は、発火日からの相対計算ではなく、曜日にアンカーして固定する。スケジュールが少しずれても結果が崩れない設計にしておくと安全だ。
- 設定ファイルの修正と、本番への再デプロイは別物。直しただけで安心せず、デプロイ後の出力で実際の設定を必ず確認する。