ブログ一覧

Cloudflare Workers の Cron で曜日 0 は拒否され、7 は土曜になる

更新: Web開発者向け

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 は存在していて、Cron が無いので一度も起動されない状態です。監視用の 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).

wrangler.jsonc の crons と、Worker の中で「どちらの Cron から起動されたか」を見分けるために持たせている環境変数の両方を SUN に直しました。

"triggers": {
	"crons": ["0 */3 * * *", "30 23 * * SUN"]
},
"vars": {
	"HEARTBEAT_CRON": "30 23 * * SUN"
}

Worker の中では event.cron を環境変数と比較して、3 時間ごとの定期チェックと週 1 回の生存報告を分岐しています。event.cron に登録した文字列がそのまま入る前提の実装で、7 のときはそれで動いていました。SUN でも同じ文字列が渡るかは、次の日曜の初回発火で確かめます。cron 式と環境変数のどちらか一方だけを直すと、Cron は日曜に起動するのに Worker の中では生存報告と判定されないという、エラーの出ない失敗になります。

この Worker が見張っているもの

Cron の話だけでは何のための Worker か分からないので、短く書いておきます。

Cloudflare D1 の無料枠は 1 日 500 万行の読み取りです。8 月の下旬、8 サイトを載せた 1 アカウントで 1 日 2,110 万行(枠の 422%)を読んでいて(Cloudflare D1 の読み取りが 1 日 2,100 万行になった原因)、気づいたきっかけは Cloudflare からの警告メールでした。原因はサイドバーの件数集計(キャッシュを 30 日にしても D1 の読み取りが減らない原因)、ミドルウェアの先読み、タームページの集計と、すべてコードの側にあり、テンプレートとキャッシュの見直しで 1 日 156 万〜196 万行(31〜39%)まで下げました。

問題は、同じ劣化がまた起きても次も警告メールが来るまで気づけないことでした。そこで、読み取り行数が対策前の水準に戻っていないかを見張る Worker を作りました。

  • 先行指標は「1 起動あたりの読み取り行数」。 日次の合計はアクセス量で増減するので、アクセスの少ない日は正常に見えます。1 起動あたりの行数はアクセス量に左右されず、コードが原因の劣化がそのまま数字に出ます。実際、8 月の障害はトラフィックの監視(Google Analytics を入れずに Cloudflare でブログを監視する)には一切現れませんでした
  • 閾値はサイト別のベースラインの 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 時間に数えられず、処理を分割する必要はありませんでした。

Cron の曜日は名前で書き、デプロイ後に登録内容を確かめる

  • 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 は、何も通知しない状態を故障と区別できない。生存報告を出す Cron を 1 本持たせ、報告が来なかったら故障を疑う

生存報告の Cron が土曜に登録されていた 2 日間は、まだ土曜が来ていなかったので実害は出ていません。実害が出る前に見つかったのは、記事を書くためにドキュメントを読み直したからで、運用の手順で見つけたわけではありませんでした。

この記事をシェア