Cloudflare Workers の Cron で曜日 0 は拒否され、7 は土曜になる
Cloudflare Workers に Cron Trigger を 2 本持つ監視用の Worker をデプロイしたら、GitHub Actions のジョブが赤くなりました。ところが Worker 自体は本番に上がっていて、動いてはいる。ただし Cron が 1 本も登録されていない、という中途半端な状態でした。
原因は cron 式の曜日でした。そして直したつもりの直し方も、もう一段間違えていました。その記録です。
デプロイが「半分だけ」失敗する
wrangler deploy のログです。
Total Upload: 43.82 KiB / gzip: 12.00 KiB
env.HEARTBEAT_CRON ("30 23 * * 0") Environment Variable
Uploaded emdash-monitor (0.74 sec)
No targets deployed for emdash-monitor (0.86 sec)
✘ [ERROR] Trigger configuration for "emdash-monitor" was only partially updated:
Cron schedules:
- A request to the Cloudflare API (/accounts/<account-id>/workers/scripts/emdash-monitor/schedules) failed.
- invalid cron string: 30 23 * * 0 [code: 10100]
Successful trigger changes were not rolled back.Uploaded emdash-monitor まで成功したあとに、schedules の登録だけが失敗しています。最後の行にあるとおり、成功した分は巻き戻されません。API で確かめると、スクリプトは存在して Cron は空でした。
$ curl .../workers/scripts/emdash-monitor/schedules
{"result":{"schedules":[]},"success":true}ジョブの赤だけを見て「デプロイされなかった」と読むと実態を取り違えます。Worker は居る、ただし何も起きない、という状態です。監視用の Worker なので、何も起きないことに気づく手段が無いのが一番困る点でした。
0 は拒否される。では 7 は日曜か
30 23 * * 0 は、Linux の cron でも GitHub Actions の schedule でも「毎週日曜 23:30」です。同じリポジトリの monitor.yml では 0 23 * * 0 がそのまま動いています。Cloudflare に持っていったら invalid cron string でした。
API を直接叩いて確かめると、7 と SUN と sun は通り、0 だけが弾かれます。そこで 7 に書き換え、schedules に載ったことを確認して終わりにしました。
$ curl .../workers/scripts/emdash-monitor/schedules
[{"cron":"0 */3 * * *"},{"cron":"30 23 * * 7"}]2 日後、別の記事のために公式ドキュメントを読み直していて、この 1 行を見つけました。
Days of the week go from 1 = Sunday to 7 = Saturday, which is different on some other cron systems (where 0 = Sunday and 6 = Saturday).
Cloudflare の曜日は 1 が日曜で、7 は土曜です。Linux の cron の 0〜6 とも、7 を日曜の別名として認める実装とも違います。私は「0 が駄目なら 7」という他の cron の常識でそのまま置き換えて、週 1 回の生存報告を土曜の 23:30 UTC(日曜の 8:30 JST)に登録していました。
ドキュメントは続けてこう勧めています。
To avoid ambiguity you may prefer to use the three-letter abbreviations (e.g. SUN rather than 1).SUN に直しました。wrangler.jsonc の crons と、Worker の中で「どちらの Cron から起動されたか」を見分けるために持たせている環境変数の両方です。
"triggers": {
"crons": ["0 */3 * * *", "30 23 * * SUN"]
},
"vars": {
"HEARTBEAT_CRON": "30 23 * * SUN"
}Worker の中では event.cron を環境変数と比較して、3 時間ごとの定期チェックと週 1 回の生存報告を分岐しています。event.cron に登録した文字列がそのまま入る前提の実装で、7 のときはそれで動いていました。SUN でも同じ文字列が渡るかは、次の日曜の初回発火で確かめます。ここを片方だけ直すと、Cron は日曜に鳴るのに Worker の中では生存報告と認識されない、という別の静かな失敗になります。
この Worker が見張っているもの
Cron の話だけでは何のための Worker か分からないので、短く書いておきます。
Cloudflare D1 の無料枠は 1 日 500 万行の読み取りです。8 月の下旬、8 サイトを載せた 1 アカウントで 1 日 2,110 万行(枠の 422%)を読んでいて、気づいたきっかけは Cloudflare からの警告メールでした。原因はサイドバーの件数集計、ミドルウェアの先読み、タームページの集計と、すべてコードの側にあり、テンプレートとキャッシュの見直しで 1 日 156 万〜196 万行(31〜39%)まで下げました。
問題は、同じことをもう一度やっても、次も警告メールで気づくことでした。そこで「減らす仕組み」ではなく「戻っていないことを見張る仕組み」として、この Worker を作りました。
- 先行指標は「1 起動あたりの読み取り行数」。 日次の合計はアクセスの波に埋もれ、暇な日は正常に見えます。1 起動あたりはアクセス量に左右されず、コード由来の劣化にそのまま出ます。実際、8 月の障害はトラフィックの監視には一切現れませんでした
- 閾値はサイト別のベースラインの 3 倍。 平常時のばらつきが最大 2.5 倍あり、2 倍だと小さいサイトが誤検知しました。実際の劣化は 4〜16 倍だったので、3 倍で分離できます
- 3 時間ごと。 対策前の最悪の 1 時間は持続可能レートの 8.39 倍で、そのペースが続くと 2.9 時間で枠が尽きます。対処の時間を引いて 3 時間にしました
- 正常時は無音。 異常時だけ Slack と GitHub Issue に出し、同じラベルの Issue が開いている間はコメントを積むだけにする。人間が Issue を閉じる操作がそのまま状態のリセットになります
- 週 1 回だけ、正常でも生存報告を出す。 無音を正常のサインにすると、故障と無音の区別がつきません。今回の Cron の間違いは、まさにこの生存報告の側で起きました
Cloudflare の GraphQL Analytics API(d1AnalyticsAdaptiveGroups と workersInvocationsAdaptive)を Worker から叩いているだけなので、本番サイトには一切アクセスしません。GitHub Actions からの死活チェックが Bot Fight Mode に弾かれて 403 になった ことがあり、本番に HTTP を出さない監視にした理由の一つです。無料プランのスケジュール実行は CPU 10ms ですが、API の往復は I/O で CPU 時間に乗らず、分割は不要でした。
この構成で気をつけること
- Cloudflare の Cron Trigger の曜日は
1= 日曜 〜7= 土曜。0はinvalid cron stringで拒否され、7は通るが土曜になる - 曜日は
SUNのような名前で書く。数字で書くと、他の cron に慣れた自分が読み間違える wrangler deployは Cron の登録だけ失敗してもスクリプトは上がる。ジョブが赤いときはGET /accounts/{acc}/workers/scripts/{name}/schedulesで登録内容を見るevent.cronの文字列で分岐しているなら、cron 式を直すときは比較先も一緒に直す- 監視の Worker は「何も起きない」が故障と区別できない。生存報告を 1 本持たせて、それが鳴らなかったら疑う
生存報告の Cron が土曜に登録されていた 2 日間は、まだ土曜が来ていなかったので実害は出ていません。実害が出る前に見つかったのは、記事を書くためにドキュメントを読み直したからで、運用の手順で見つけたわけではありませんでした。