scripts/preview-realtime.py ほか daily-sync 系スクリプトの起動を
GitHub Actions schedule から Cloud Scheduler + Cloud Run Jobs に移すための
インフラ一式です。
GitHub Actions の schedule: イベントは混雑時に間引かれ、5 分粒度では
事実上 1 時間に 1 回しか発火しないことがあるため、より精度の高いトリガとして
GCP の Cloud Scheduler から Cloud Run Jobs を直接叩く構成にしています。
現在 Cloud Run Jobs に載っている処理:
| Job 名 | スケジュール | 旧 GHA workflow | 概要 |
|---|---|---|---|
preview-realtime |
JST 08:00–22:55 / 5 分毎 | .github/workflows/preview-realtime.yml (schedule: 削除済み、workflow_dispatch のみフォールバック) |
直前バッチ + index 更新 + 結果取り込み |
daily-sync |
JST 07:30 / 1 日 1 回 | .github/workflows/daily-sync.yml (移行完了後に削除) |
K-file 結果 + 当日 race_cards / recent_form / motor_stats / title 取得 + daily index 生成 |
monthly-weights |
JST 06:00 / 毎月 1 日 | .github/workflows/monthly-weights.yml (移行完了後に削除) |
active な全予想者について、直近 6 ヶ月の特徴量から場ごとの n_components 要素重みを再計算し data/estimate/stadium/weights/{predictor_id}/YYYY-MM.csv を更新 (daily-sync 直前に走らせ、境界日の daily/realtime index を同一 weights で計算)。あわせて静的テーブル data/estimate/{suji,kimarite}/tables/*.csv (スジ表・決まり手セル係数・Stage2 ペア表・校正・log-loss) も再生成して commit する |
Cloud Scheduler (Asia/Tokyo)
│ preview-realtime-daytime: */5 8-22 * * * → preview-realtime Job
│ daily-sync: 30 7 * * * → daily-sync Job
│ monthly-weights: 0 6 1 * * → monthly-weights Job
│ HTTP POST + OIDC token (preview-realtime-invoker SA)
▼
Cloud Run Jobs (1 image, 3 jobs — switch by --command):
├─ preview-realtime (--command=/app/run.sh, cpu=1, mem=512Mi, timeout=300s)
├─ daily-sync (--command=/app/run-daily-sync.sh, cpu=2, mem=2Gi, timeout=3600s)
└─ monthly-weights (--command=/app/run-monthly-weights.sh, cpu=2, mem=2Gi, timeout=3600s)
│
▼ (Runner SA: preview-realtime-runner)
git clone --depth 1 --filter=blob:none + sparse-checkout (PAT in Secret Manager)
│
▼
preview-realtime: python scripts/preview-realtime.py
daily-sync: 6 scripts (result, race-card, recent-form, motor-stats,
race-title, build_index) + commit + GCS publish
monthly-weights: build_course_rate.py + build_suji_table.py + build_kimarite.py
+ build_kimarite_pairs.py + build_kimarite_calibration.py
+ build_kimarite_logloss.py + build_weights.py --month YYYY-MM
+ commit (weights + 静的テーブル)
│ ├─ git commit && git push origin main (boatrace.git_operations / bash)
│ ├─ ★ GCS mirror upload (boatrace.gcs_publisher.upload_csvs) ← preview-realtime / daily-sync のみ
│ │ gs://${BOATRACE_GCS_CSV_BUCKET}/data/{programs/title,programs/race_cards,
│ │ programs/{recent_national,recent_local,
│ │ waku10,motor_stats},
│ │ previews/{stt,tkz,sui,
│ │ original_exhibition,tokuten_hayami},
│ │ estimate/{predictor_id},
│ │ estimate/motor_pt/{runs,motors,baseline},
│ │ results/realtime,results/payouts}/...
│ │ (active 予想者ごとに csv_type=index:{predictor_id} で 1 件ずつ mirror)
│ │ + estimate/stadium/win_rate.csv (csv_type=waku_table)
│ │ + estimate/stadium/sui_params.csv (csv_type=sui_params)
│ │ + estimate/stadium/weights/{predictor_id}/YYYY-MM.csv
│ │ (csv_type=weights:{predictor_id}。日付非依存の静的テーブルなので
│ │ md5 一致でスキップされ、実際に上がるのは月 1 回)
│ └─ ★ Pub/Sub publish (boatrace.gcs_publisher.publish_realtime_completed) ← preview-realtime / daily-sync のみ
│ topic: ${BOATRACE_PUBSUB_TOPIC} (e.g. fun-site-realtime-completed)
│ trigger: "daily-bootstrap" (daily-sync) / "realtime" (preview-realtime)
▼
fun-site が Eventarc 経由で Cloud Run Job として起動 → Astro 再ビルド → Cloud Storage 配信
(monthly-weights 自体は git push のみ。生成された weights CSV と静的テーブルは
build_index.py / build_kimarite_*.py がリポジトリから読む。ただし win_rate.csv /
sui_params.csv / weights/{predictor_id}/ の 3 種だけは翌日以降の preview-realtime /
daily-sync が GCS へ相乗りで mirror する — fun-site の枠番詳細ページが枠番pt の内訳
raw → z → 偏差値 → 寄与 を、気象詳細ページが気象pt の内訳
特徴量 × 係数 → z → 偏差値 → 寄与 を再現するのに読むため)
boatrace-487212 (Project Number: 530399381543)asia-northeast1BoatraceCSV/boatracecsv.github.ioboatrace-realtime-data-boatrace-487212 (fun-site/infra で provision)projects/boatrace-487212/topics/fun-site-realtime-completed
(fun-site/infra で provision)| ファイル | 役割 |
|---|---|
../infra/Dockerfile |
Python 3.11-slim ベースの実行イメージ (preview-realtime / daily-sync / monthly-weights 共用) |
../infra/run.sh |
preview-realtime Job のエントリポイント (clone → sparse-checkout → python 実行) |
../infra/run-daily-sync.sh |
daily-sync Job のエントリポイント (clone → sparse-checkout → 6 スクリプト直列 → commit → GCS publish) |
../infra/run-monthly-weights.sh |
monthly-weights Job のエントリポイント (clone → sparse-checkout 8 ヶ月分 + 全履歴 [race_cards は checkout 後に追加] → build_course_rate.py → build_suji_table.py → build_kimarite{,_pairs,_calibration,_logloss}.py → build_weights.py → commit) |
../infra/cloudbuild.yaml |
Cloud Build パイプライン (build → push → 3 job 更新) |
../infra/.dockerignore |
ビルドコンテキスト最小化 |
run.sh)Cloud Run Job の 512 MiB メモリ制約のためフルクローンせず、preview-realtime.py
が実際に読み書きする領域だけを cone-mode sparse-checkout で取得します。
スクリプト側で参照ファイルが増えたら run.sh の git sparse-checkout set
リストを忘れずに拡張してください(さもないと index_csv_missing 等のログを
吐いて該当処理が静かにスキップされます)。
| 取得対象 | 用途 |
|---|---|
scripts/ |
preview-realtime.py / build_index.py / boatrace パッケージ |
.boatrace/ |
実行時設定 (load_config) |
data/estimate/stadium/ |
win_rate.csv, sui_params.csv, weights/{predictor_id}/*.csv |
data/estimate/<predictor_id>/<YYYY/MM>/ |
当日 index CSV(直前バッチで一部レースを更新)。ACTIVE_PREDICTORS 配列でループ |
data/programs/recent_national/<YYYY/MM>/ |
全国近況5節 |
data/programs/recent_local/<YYYY/MM>/ |
当地近況5節 |
data/programs/motor_stats/<YYYY/MM>/ + <前月>/ |
モーター期成績(前月分は7日fallback用) |
data/previews/{tkz,stt,sui,original_exhibition,tokuten_hayami}/<YYYY/MM>/ |
直前バッチの追記対象 (preview-realtime.py の PREVIEW_SOURCES と 1:1)。cone 外のパスが 1 つでも混ざると git add がまとめて失敗し、その回の commit が丸ごと落ちる |
data/previews/{od1,od2,od3}/<YYYY/MM>/ |
集計中オッズの追記対象。cone 外だと git add が無視され永続化されない |
data/results/realtime/<YYYY/MM>/ |
bc_rs1_2 由来の realtime 結果 CSV(締切後の追記対象)。cone 外だと git add が無視され永続化されない |
data/results/payouts/<YYYY/MM>/ |
bc_rs2 由来の払戻金 CSV(締切後の追記対象)。同じく cone 外だと git add が無視され永続化されない |
run-daily-sync.sh)daily-sync は preview-realtime とは別系統の入出力を扱うため、run-daily-sync.sh
側で別の sparse-checkout リストを持っています(data/previews/ や
data/results/realtime/ は不要、代わりに data/programs/race_cards/ 等が必要)。
| 取得対象 | 用途 |
|---|---|
scripts/ |
race-card.py / recent-form.py / motor-stats.py / race-title.py / build_index.py / build_motor_pt_breakdown.py / boatrace パッケージ |
.boatrace/ |
実行時設定 (load_config) |
data/estimate/stadium/ |
win_rate.csv, sui_params.csv, weights/{predictor_id}/*.csv (build_index 入力) |
data/estimate/<predictor_id>/<YYYY/MM>/ |
build_index –mode daily –all-active の出力先 (各 active 予想者を ACTIVE_PREDICTORS 配列でループ。commit 対象) |
data/programs/race_cards/<YYYY/MM>/ |
race-card.py の出力先 (GCS ミラー対象) |
data/programs/waku10/<YYYY/MM>/ |
race-card.py の出力先 (bc_j_waku10 枠番別過去10走) |
data/programs/monthly_schedule/ |
race-card.py の出力先 (bc_mon_2 月間開催日程。月ファイル上書きのため全体を取得) |
data/programs/recent_national/<YYYY/MM>/ |
recent-form.py の出力先 / build_index 特徴量 |
data/programs/recent_local/<YYYY/MM>/ |
recent-form.py の出力先 / build_index 特徴量 |
data/programs/motor_stats/<YYYY/MM>/ + <前月>/ |
motor-stats.py の出力先 (前月分は build_index の 7 日 fallback 用) |
data/programs/motor_history/<YYYY/MM>/ + <前月>/ |
motor-stats.py の出力先 (bc_mrireki モーター履歴。パス日付=前節終了日のため前月分も対象) |
data/programs/title/<YYYY/MM>/ |
race-title.py の出力先 (GCS ミラー対象) |
data/programs/race_cards/ + title/ の 90 日窓ぶんの月 |
モーターptの 90 日ルックバック (MOTOR_HISTORY_LOOKBACK_DAYS)。各場の直近 6 節の検出と、各節最終日の節間成績 + グレード読み出しに使う。欠けると採用節数が落ちてモーターptが平均 50 側に潰れる (月初は 0 節 = 全モーターptが 50)。motor_lookback_months として 90 日窓が触る月を実際に列挙する (短い月が続くと 4 ヶ月前まで届くため固定の月数にはしない。例: 5/1 の 90 日前は 1/31) |
data/estimate/motor_pt/{runs,motors,baseline}/<YYYY/MM>/ |
build_motor_pt_breakdown.py の出力先 (モーターpt 素点の内訳。commit + GCS ミラー対象) |
preview-realtime 側はこの 90 日ぶんを 取らない。5 分毎に走るジョブで 毎回 20MB 近い race_cards を引くのは割に合わないため、index CSV の
realtime行は朝バッチが計算したモーターptを再利用する (build_index.DAILY_REUSED_COMPONENTS)。素点は当日中に変化しないので 引き継いでも値は変わらない。詳細はoperations.md。
run-monthly-weights.sh)monthly-weights は build_weights.py が直近 6 ヶ月の全日について
compute_features_for_day(repo, day) を呼ぶため、対象月 (target_month) +
過去 6 ヶ月 + motor_stats の 7 日 fallback 用にさらに 1 ヶ月、計 8 ヶ月分
の月単位ディレクトリを sparse-checkout する。月数の計算は
run-monthly-weights.sh の bash ループで自動生成される。
同じジョブで動く全履歴スクリプト (build_course_rate / build_suji_table /
build_kimarite*) はこの 8 ヶ月窓では足りないので、月に依存しないパスを
ディレクトリごと cone に入れる。唯一 data/programs/race_cards/ だけは
checkout 後の 2 段階目で必要な月だけ追加する (理由は下記)。
| 取得対象 | 用途 |
|---|---|
scripts/ |
build_weights.py / boatrace パッケージ (index_features) |
.boatrace/ |
実行時設定 (load_config) |
data/estimate/stadium/ |
win_rate.csv / sui_params.csv / course_win_rate.csv (build_weights 入力) + weights/{predictor_id}/ (出力先) |
data/results/realtime/ |
全履歴 (build_course_rate.py / build_suji_table.py / build_kimarite*.py が全期間を舐める。月別 sparse とは別に静的に全期間 checkout) |
data/previews/stt/ |
全履歴 (進入コース。build_suji_table / build_kimarite_pairs / build_kimarite_calibration / build_kimarite_logloss が全期間で引く。欠けると entry_courses が枠なり進入にフォールバックし、スジ表・決まり手セル・log-loss が黙って歪む) |
data/previews/tkz/ |
全履歴 (展示タイム。build_kimarite.py が results/realtime の全日について引く特徴量) |
data/previews/sui/ |
全履歴 (気象。同上) |
data/estimate/suji/tables/ |
出力先 — build_suji_table.py の {suji_table,kimarite_table}.csv |
data/estimate/kimarite/ |
出力先 + 入力 — tables/ が build_kimarite / build_kimarite_pairs / build_kimarite_calibration / build_kimarite_logloss の出力先、YYYY/MM/ の日次予測 CSV が calibration と log-loss の入力。両方要るので親ごと cone に入れる (全体で数 MB) |
data/estimate/v10_kimarite/ |
build_kimarite_logloss.py の PL ベースライン用 強さpt。同スクリプトの PREDICTOR_ID と同期させること |
data/results/realtime/<YM>/ × 8 |
着順 (target = 7 - 着順) |
data/programs/race_cards/<YM>/ × 8 |
レース宇宙 (universe) |
data/programs/recent_national/<YM>/ × 8 |
recent 特徴量 (racer 列) |
data/programs/recent_local/<YM>/ × 8 |
recent 特徴量 (racer 列) |
data/programs/motor_stats/<YM>/ × 8 |
motor 特徴量 (8 ヶ月目は月初 7 日 fallback 用) |
data/previews/sui/<YM>/ × 8 |
気象 (weather 特徴量) |
data/previews/tkz/<YM>/ × 8 |
展示タイム (exhibit 特徴量) |
data/previews/stt/<YM>/ × 8 |
進入コース (course 補正) |
data/previews/original_exhibition/<YM>/ × 8 |
展示値 1〜3 (exhibit 特徴量) |
data/programs/race_cards/<YM>/ × 全月 (2 段階目) |
build_kimarite.py の universe。git checkout 後に data/results/realtime/ の実ディレクトリから月を導出し、git sparse-checkout add で追加する |
⚠️
data/previews/*を入れ忘れると_load_realtime_preview_by_codeが空 dict を返し、exhibitとweather特徴量が全レースで NaN になる。するとfit_oneのdropna(subset=["waku","racer","exhibit","weather","着順"])で 全行が落ちて n=0 FALLBACK が 24 場分発生する症状になるため、4 ファミリすべて 必須。
⚠️ 書き込み先を cone と
git addの両方に足すこと。 cone-mode では cone 外のパスに書いたファイルはgit addに無視され、エラーにならずに 毎月捨てられる。実際data/estimate/{suji,kimarite}/tables/は導入時から どちらにも入っておらず、再生成された静的テーブルが永続化されていなかった (logloss.csvに至ってはリポジトリに 1 度も存在しなかった)。静的テーブルを 吐くスクリプトを追加したら、run-monthly-weights.shのpaths配列と 末尾のgit addを必ずセットで更新する。
⚠️
build_kimarite.pyの学習母数は cone の広さで決まる。 このスクリプトだけはrace_cardsとpreviews/{tkz,sui}も日ごとに引き、 race_cards が無い日はcontinueでまるごと無言スキップする。8 ヶ月窓 しか cone に無かった頃は docstring の「学習窓 = 全履歴」に反して母数が直近 8 ヶ月になっていた (2026-09-01 実行相当で 43,595 → 30,160 レース = 31% 欠落。 しかもdata/results/realtime/が 2025/11 開始なので欠落幅は毎月 1 ヶ月ずつ 拡大する)。2026-08-22 に 3 ファミリとも全履歴を cone に入れて解消済み。 ジョブログのraces=NNNNN (YYYY-MM-DD 〜 YYYY-MM-DD)の左端がdata/results/realtime/の最古日と一致しているかで再発を検知できる。
data/previews/{stt,tkz,sui}/ は data/results/realtime/ と同じ 2025/11 開始
なので、ディレクトリごと cone に入れても取得量は「必要な月ぶん」と変わらない。
一方 data/programs/race_cards/ は 2025/05 開始で、2025/05〜2025/10 の約
42MB は results/realtime より前にあたり build_kimarite からは 1 日も参照
されない。Cloud Run の /tmp は tmpfs で checkout サイズがそのまま memory を
食うため、race_cards だけは git checkout 後に results/realtime の実月から
導出して git sparse-checkout add する。epoch を定数で持たないので、履歴が
伸びても backfill されても追従する。
| cone | tmpfs (worktree + .git) |
|---|---|
| 全履歴対応前 | 約 304MB |
| 本構成 (race_cards は必要な月のみ) | 約 321MB (+9MB/月) |
| race_cards を丸ごと入れた場合 | 約 374MB (うち 42MB は死荷重) |
| 項目 | 実測 | 伸び方 |
|---|---|---|
| checkout (tmpfs 上) | 約 321MB | 全履歴ファミリぶん +9MB/月 |
build_kimarite.py の peak RSS |
578MB (43,595 レース) | 約 9.6KB/レース = +42MB/月 |
| 全履歴スクリプト 5 本の実行時間 | 合計 ~7 秒 | ほぼ線形 |
--memory=2Gi に対して現状の山は約 0.9GB。上の伸び率だと約 2 年で上限に
近づくので、build_kimarite.py の peak RSS が 1.2GB を超えたら
infra/cloudbuild.yaml の deploy-job-monthly-weights を 4Gi に上げる。
target_month を TARGET_MONTH=2026-05 のように上書きすると、その月を起点と
した 8 ヶ月分が動的に cone に入る (2 段階目の race_cards は target_month に
依らず results/realtime の全月)。月数を変えたい場合は
run-monthly-weights.sh の冒頭ループ for i in 1 2 3 4 5 6 7 を編集する。
以下は初回のみ実行する gcloud コマンド集です。Cloud Shell か、
gcloud auth login 済みのローカル端末から実行してください。
export PROJECT_ID=boatrace-487212
export PROJECT_NUMBER=530399381543
export REGION=asia-northeast1
export AR_REPO=containers
export IMAGE=preview-realtime
export JOB_NAME=preview-realtime
export RUNNER_SA=preview-realtime-runner
export INVOKER_SA=preview-realtime-invoker
export SECRET_NAME=github-token
export GITHUB_REPO=BoatraceCSV/boatracecsv.github.io
gcloud config set project "$PROJECT_ID"
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
cloudscheduler.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
iam.googleapis.com \
logging.googleapis.com
gcloud artifacts repositories create "$AR_REPO" \
--repository-format=docker \
--location="$REGION" \
--description="Container images for boatrace automation"
# Job 実行用 (ワークロード ID)
gcloud iam service-accounts create "$RUNNER_SA" \
--display-name="preview-realtime Cloud Run Job runner"
# Scheduler が Job を叩くための ID
gcloud iam service-accounts create "$INVOKER_SA" \
--display-name="Cloud Scheduler invoker for preview-realtime"
GitHub 側で fine-grained PAT を発行します。
BoatraceCSV/boatracecsv.github.io のみContents = Read and write発行したトークンを貼り付け:
printf '%s' 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | \
gcloud secrets create "$SECRET_NAME" \
--replication-policy=automatic \
--data-file=-
# Job 実行 SA に読み取り権限
gcloud secrets add-iam-policy-binding "$SECRET_NAME" \
--member="serviceAccount:${RUNNER_SA}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/secretmanager.secretAccessor
更新時:
gcloud secrets versions add "$SECRET_NAME" --data-file=-Cloud Run Job 側で--update-secrets=GITHUB_TOKEN=${SECRET_NAME}:latestとしていれば自動で最新版が参照されます。
scripts/boatrace/gcs_publisher.py が GCS 書込と Pub/Sub publish を行う。
バケットと topic 自体は fun-site/infra で terraform apply 済みである前提。
環境変数 BOATRACE_GCS_CSV_BUCKET / BOATRACE_PUBSUB_TOPIC は
infra/cloudbuild.yaml の substitutions (_GCS_CSV_BUCKET / _PUBSUB_TOPIC)
として宣言してあり、deploy-job ステップの --set-env-vars で毎ビルド注入される。
そのため手動で gcloud run jobs update --update-env-vars=... を実行する必要はない (値を
変えたい場合は cloudbuild.yaml の substitutions を書き換えて再ビルド)。
gcloud run jobs deployの--set-env-varsは列挙したキーで全置換するため、 過去にこの README では手動--update-env-varsを案内していた。それを毎回手で 入れ直す運用になっていたのを cloudbuild.yaml に取り込んで自動化した経緯。 既存の Job に--update-env-varsで個別追加した値も次回ビルドで上書きされるので、 値を変えたい場合は必ずcloudbuild.yaml側を編集すること。
Runner SA への IAM ロール (
roles/storage.objectAdminon the bucket とroles/pubsub.publisheron the topic) は fun-site/infra/realtime-pipeline.tf のgoogle_storage_bucket_iam_member.preview_realtime_csv_writer/google_pubsub_topic_iam_member.preview_realtime_publisherで付与される。 二重管理を避けるため、本リポジトリ側では IAM 付与のgcloudを実行しない。
未設定時は gcs_publisher が gcs_upload_skipped reason=no_bucket_configured 等の
ログを出して no-op になるため、段階的ロールアウト(コードだけ先に main にマージし、
本番側で環境変数を後から付与)が可能。
RUNNER="serviceAccount:${RUNNER_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
# --- 5-a. Runner SA ---
# Cloud Logging への書き込み
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="$RUNNER" \
--role=roles/logging.logWriter
# --- 5-b. Cloud Build のデフォルト SA ---
# cloudbuild.yaml の deploy-job ステップで Cloud Run Job を作成/更新するため、
# Cloud Build SA に下記の権限が必要。これが無いと
# "build step ... gcr.io/google.com/cloudsdktool/cloud-sdk:slim failed:
# step exited with non-zero status: 1" でコケる典型ケース。
CB_SA="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# 旧プロジェクトでは下記のレガシー SA がデフォルト:
# ${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com
# `gcloud builds list` の "createdBy" でどちらが実際に使われているか確認可。
# Cloud Run Jobs を deploy/update する権限
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="$CB_SA" \
--role=roles/run.developer
# Job に Runner SA を attach する権限 (Service Account User)
gcloud iam service-accounts add-iam-policy-binding \
"${RUNNER_SA}@${PROJECT_ID}.iam.gserviceaccount.com" \
--member="$CB_SA" \
--role=roles/iam.serviceAccountUser
cloudbuild.yaml は gcloud run jobs deploy を使っているため、Job が
存在しなければ作成、存在すれば更新します。リポジトリ ルートで実行:
gcloud builds submit \
--config infra/cloudbuild.yaml \
--substitutions=SHORT_SHA=$(git rev-parse --short HEAD) \
--project "$PROJECT_ID"
build 完了後、Job が出来ていることを確認:
gcloud run jobs describe "$JOB_NAME" --region="$REGION" --format='value(name,template.template.containers[0].image)'
INVOKER="serviceAccount:${INVOKER_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud run jobs add-iam-policy-binding "$JOB_NAME" \
--region="$REGION" \
--member="$INVOKER" \
--role=roles/run.invoker
preview-realtime-daytime 1 本を JST 08:00 起点・5 分毎で登録します
(過去には JST 08:30 系列を別 Scheduler preview-realtime-morning として
切っていましたが、開催日朝の getHoldingList2 で扱える時刻が前倒し
された関係で 08:00 起点に統合し、preview-realtime-morning は削除済み)。
JOB_URI="https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/${JOB_NAME}:run"
INVOKER_EMAIL="${INVOKER_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
# JST 08:00, 08:05, ..., 22:55
gcloud scheduler jobs create http preview-realtime-daytime \
--location="$REGION" \
--schedule="*/5 8-22 * * *" \
--time-zone="Asia/Tokyo" \
--uri="$JOB_URI" \
--http-method=POST \
--oauth-service-account-email="$INVOKER_EMAIL" \
--oauth-token-scope="https://www.googleapis.com/auth/cloud-platform" \
--attempt-deadline=60s \
--description="Preview realtime — JST 08:00-22:59"
既存の Scheduler の間隔を変える場合は update を使います:
gcloud scheduler jobs update http preview-realtime-daytime \
--location="$REGION" --schedule="*/5 8-22 * * *"
5 分毎なのはコスト削減のため (2026-09 に 2 分毎から戻した)。
preview-realtime.pyの直前情報の取得窓は締切[1, 10]分前で、5 分毎でも 各レースが最低 1 回は窓に入るため取りこぼしは出ない。反映の遅れは 最大 2 分から最大 5 分に伸びる。稼働時間帯は締切 08:32 のモーニングと 22:45 のミッドナイトを両方拾うため 08:00〜22:55 から削らない。
attempt-deadlineは Scheduler がjobs:runAPI のレスポンスを待つ時間 です。Cloud Run Jobs の:runは実行をキックしてすぐ返るので 60 秒で十分。
# 手動キック
gcloud run jobs execute "$JOB_NAME" --region="$REGION" --wait
# 直近の実行ログ
gcloud beta run jobs executions list --job="$JOB_NAME" --region="$REGION" --limit=5
gcloud beta run jobs executions describe <EXECUTION_NAME> --region="$REGION"
# Scheduler を即座に発火
gcloud scheduler jobs run preview-realtime-daytime --location="$REGION"
# Cloud Logging で stdout/stderr を見る
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="preview-realtime"' \
--limit=50 --format='value(timestamp,textPayload)'
preview-realtime が稼働済みである前提。daily-sync Job は同じイメージ・
同じ Runner SA / Invoker SA / Cloud Build パイプラインを再利用する設計
なので、新規に必要なのは (a) Job への invoker 権限付与 と
(b) Cloud Scheduler 1 本 だけ。
infra/cloudbuild.yaml に deploy-job-daily-sync ステップが入った
状態で 1 回ビルドが走り、gcloud run jobs describe daily-sync で
Job が存在することを確認済み(無ければ
gcloud builds submit --config infra/cloudbuild.yaml --substitutions=SHORT_SHA=$(git rev-parse --short HEAD)
を実行して作る)。.github/workflows/daily-sync.yml の schedule: はまだ削除していない
(Cloud Scheduler を --paused で作って動作確認 → resume 直前に GHA 側
schedule: を削除する、という順序で進める)。INVOKER_EMAIL="${INVOKER_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud run jobs add-iam-policy-binding daily-sync \
--region="$REGION" \
--member="serviceAccount:${INVOKER_EMAIL}" \
--role=roles/run.invoker
Runner SA (
preview-realtime-runner) の Secret / Logging / GCS / Pub/Sub 権限は preview-realtime 用に既に付与済みなので、daily-sync Job も そのまま動く。新規付与は不要。
--paused で作成JOB_URI_DAILY="https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/daily-sync:run"
gcloud scheduler jobs create http daily-sync \
--location="$REGION" \
--schedule="30 7 * * *" \
--time-zone="Asia/Tokyo" \
--uri="$JOB_URI_DAILY" \
--http-method=POST \
--oauth-service-account-email="$INVOKER_EMAIL" \
--oauth-token-scope="https://www.googleapis.com/auth/cloud-platform" \
--attempt-deadline=60s \
--max-retry-attempts=3 \
--min-backoff=30s \
--max-backoff=300s \
--description="Daily boatrace data sync — JST 07:30"
# 動作確認まで pause しておく
gcloud scheduler jobs pause daily-sync --location="$REGION"
JST 07:30 = UTC 22:30。
--time-zone=Asia/Tokyo指定で cron は JST ネイティブに30 7 * * *と書ける。旧 GHA の30 22 * * *(UTC) と 同じタイミング。
# 1 サイクル手動実行 (実測 ~22 分待機)
gcloud run jobs execute daily-sync --region="$REGION" --wait
# 直近の実行ログ
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="daily-sync"' \
--limit=80 --freshness=30m \
--format='value(timestamp,textPayload)'
# git に commit が積まれたか確認
git -C /tmp/boatracecsv.github.io log --oneline -10 # クローンしてある場合
# GCS ミラーが届いたか
gsutil ls "gs://${BOATRACE_GCS_CSV_BUCKET}/data/programs/title/$(TZ=Asia/Tokyo date +'%Y/%m')/"
gsutil ls "gs://${BOATRACE_GCS_CSV_BUCKET}/data/programs/race_cards/$(TZ=Asia/Tokyo date +'%Y/%m')/"
# Pub/Sub 配信履歴 (fun-site 側 Eventarc trigger 起動)
# fun-site のビルド履歴を確認するか、Pub/Sub topic の "Messages" メトリクスを見る
期待される stdout の最後付近 (run-daily-sync.sh のログ):
[run-daily-sync ...] STEP OK: build-index
[run-daily-sync ...] STEP OK: commit-index
[publisher] uploaded=N updated_races=M trigger=daily-bootstrap
[publisher] published message_id=...
[run-daily-sync ...] STEP OK: gcs-publish
[run-daily-sync ...] Done (aggregate exit=0)
両方が同じ日に同じ CSV ファミリへ書くと git push 競合や CSV ダブリの 原因になるため、先に GHA 側 schedule を停止してから Cloud Scheduler を resume する。
# (a) GHA 側 schedule を削除する PR を merge (.github/workflows/daily-sync.yml 削除)
# → PR #3 として別途実施
# (b) Cloud Scheduler を resume
gcloud scheduler jobs resume daily-sync --location="$REGION"
# (c) 翌朝 JST 07:30 の自動実行を観察
gcloud scheduler jobs describe daily-sync --location="$REGION" \
--format='value(state,scheduleTime,lastAttemptTime)'
問題が出た場合:
# 1) Cloud Scheduler を一時停止 (即時)
gcloud scheduler jobs pause daily-sync --location="$REGION"
# 2) 必要なら GHA 側 schedule を git revert で復活
# → PR #3 を revert する PR を merge
preview-realtime / daily-sync が稼働済みである前提。monthly-weights Job も
同じイメージ・Runner SA / Invoker SA / Cloud Build パイプラインを再利用する
ので、新規に必要なのは (a) Job への invoker 権限付与 と
(b) Cloud Scheduler 1 本 だけ。
infra/cloudbuild.yaml に deploy-job-monthly-weights ステップが入った
状態で 1 回ビルドが走り、gcloud run jobs describe monthly-weights で
Job が存在することを確認済み (無ければ
gcloud builds submit --config infra/cloudbuild.yaml --substitutions=SHORT_SHA=$(git rev-parse --short HEAD)
を実行して作る)。.github/workflows/monthly-weights.yml の schedule: はまだ削除して
いない (Cloud Scheduler を --paused で作って動作確認 → resume 直前に
GHA 側 schedule: を削除する、という順序で進める)。INVOKER_EMAIL="${INVOKER_SA}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud run jobs add-iam-policy-binding monthly-weights \
--region="$REGION" \
--member="serviceAccount:${INVOKER_EMAIL}" \
--role=roles/run.invoker
Runner SA (
preview-realtime-runner) の Secret / Logging 権限は preview-realtime 用に既に付与済み。monthly-weights は GCS / Pub/Sub publish を呼ばないため、これらの追加権限は不要。
--paused で作成JOB_URI_MONTHLY="https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/monthly-weights:run"
gcloud scheduler jobs create http monthly-weights \
--location="$REGION" \
--schedule="0 6 1 * *" \
--time-zone="Asia/Tokyo" \
--uri="$JOB_URI_MONTHLY" \
--http-method=POST \
--oauth-service-account-email="$INVOKER_EMAIL" \
--oauth-token-scope="https://www.googleapis.com/auth/cloud-platform" \
--attempt-deadline=60s \
--max-retry-attempts=3 \
--min-backoff=30s \
--max-backoff=300s \
--description="Monthly index weights — JST 06:00 on the 1st of each month (before daily-sync at 07:30)"
# 動作確認まで pause しておく
gcloud scheduler jobs pause monthly-weights --location="$REGION"
JST 06:00 は daily-sync (JST 07:30) の 前 に終わらせる狙いの時刻。 境界日 (毎月 1 日) の daily index と realtime index を同一 weights で 計算するための前倒し。 訓練ウィンドウから前月末日の K-file (daily-sync が 07:30 に取り込む) が 1 日抜けるトレードオフは許容済み。
# 1 サイクル手動実行 (実測 N 分待機 — 初回ログから timeout を再評価する)
gcloud run jobs execute monthly-weights --region="$REGION" --wait
# 過去月の再計算 (backfill)
gcloud run jobs execute monthly-weights --region="$REGION" \
--update-env-vars=TARGET_MONTH=2026-03 --wait
# 直近の実行ログ
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="monthly-weights"' \
--limit=80 --freshness=1h \
--format='value(timestamp,textPayload)'
# git に commit が積まれたか確認
gh api /repos/${GITHUB_REPO}/commits?path=data/estimate/stadium/weights --jq '.[0:3] | .[] | {sha, message: .commit.message}'
# 出力 CSV の整合確認 (旧 GHA の出力と diff — 入力データ + アルゴリズムが
# 同じなら原則一致するはず)
期待される stdout の最後付近 (run-monthly-weights.sh のログ):
[run-monthly-weights ...] Extending sparse-checkout: race_cards × 7 months (2025/11 〜 2026/05) for build_kimarite.py
[run-monthly-weights ...] Retraining kimarite cell model (荒れ度メーター)
races=28000 (2025-11-01 〜 2026-04-30) classes=32
[run-monthly-weights ...] Building monthly weights for 2026-05
... (build_weights.py の per-stadium fit ログ — stderr 経由)
FeatureContext stats: race_cards=270 title=270 runs=~1350 period_starts=181
[run-monthly-weights ...] Push attempt 1/5
[run-monthly-weights ...] Push succeeded on attempt 1
[run-monthly-weights ...] Pushed weights for 2026-05
[run-monthly-weights ...] Done
差分が無かった場合は Push attempt 〜 Pushed weights ... のブロックが
無く、代わりに No weights changes to commit for ... が出る。
Push attempt N/5 が複数回出る場合は preview-realtime の commit と
レースして fetch + rebase でリトライしている状態。N=5 まで使い切ったら
ABORT: failed to push weights ... で fail する(後述のトラブルシュート参照)。
両方が同月の data/estimate/stadium/weights/{predictor_id}/YYYY-MM.csv を生成すると
無駄な走行 + git push 競合の原因になるため、先に GHA 側 schedule を
停止してから Cloud Scheduler を resume する。
# (a) GHA 側 schedule を削除する PR を merge (.github/workflows/monthly-weights.yml 削除)
# → 別 PR として実施
# (b) Cloud Scheduler を resume
gcloud scheduler jobs resume monthly-weights --location="$REGION"
# (c) 翌月 1 日 JST 06:00 の自動実行を観察
gcloud scheduler jobs describe monthly-weights --location="$REGION" \
--format='value(state,scheduleTime,lastAttemptTime)'
問題が出た場合:
# 1) Cloud Scheduler を一時停止 (即時)
gcloud scheduler jobs pause monthly-weights --location="$REGION"
# 2) 必要なら GHA 側 schedule を git revert で復活
monthly-weights の出力は active な予想者ごとの CSV
(data/estimate/stadium/weights/{predictor_id}/YYYY-MM.csv)
なので、誤って壊れた weights が push された場合は GitHub 側で当該コミットを
revert し、過去月の gcloud run jobs execute monthly-weights --update-env-vars=TARGET_MONTH=...
で再生成すれば復旧する。
scripts/preview-realtime.py や scripts/build_index.py、infra/Dockerfile、
infra/run.sh などを修正した場合の反映フロー。
| 変更したファイル | 必要なアクション |
|---|---|
scripts/*.py (Python ソース) |
イメージ再ビルド + 全 Job 更新 |
scripts/requirements.txt (新規依存追加) |
イメージ再ビルド + 全 Job 更新 |
infra/Dockerfile |
イメージ再ビルド + 全 Job 更新 |
infra/run.sh (sparse-checkout 等) |
イメージ再ビルド + preview-realtime Job 更新 |
infra/run-daily-sync.sh (sparse-checkout 等) |
イメージ再ビルド + daily-sync Job 更新 |
infra/run-monthly-weights.sh (sparse-checkout 等) |
イメージ再ビルド + monthly-weights Job 更新 |
infra/cloudbuild.yaml |
再ビルドだけで反映される(自身が実行される) |
data/** のみ |
不要(Job は実行時に最新 main を pull する) |
.github/workflows/preview-realtime.yml |
不要(Cloud Run 経路は GitHub Actions に依存しない) |
後段 で Cloud Build トリガを infra/**,scripts/** 監視で
作成済みなら、git push origin main だけで自動的にイメージ再ビルドと
Cloud Run Job のロールアウトが走ります。完了は GCP コンソールの
Cloud Build 履歴 / Cloud Run Jobs リビジョンで確認できます。
gcloud builds submit \
--config infra/cloudbuild.yaml \
--substitutions=SHORT_SHA=$(git rev-parse --short HEAD) \
--project "$PROJECT_ID"
cloudbuild.yaml の deploy-job ステップが gcloud run jobs deploy を
実行するため、Job の containers[0].image が新タグ
(${SHORT_SHA}) に置き換わります。Cloud Scheduler 側は Job 名で
参照しており、常に最新リビジョンを起動するので変更不要です。
ビルドは典型的に 2〜4 分。Cloud Build SA に
roles/run.developerと Runner SA へのroles/iam.serviceAccountUserが要ります(§5-b)。
# Job が指している現在のイメージタグを確認
gcloud run jobs describe "$JOB_NAME" \
--region="$REGION" \
--format='value(template.template.containers[0].image)'
# 手動キックして 1 サイクル動作確認 (--wait で完了まで待機)
gcloud run jobs execute "$JOB_NAME" --region="$REGION" --wait
# 直近の実行ログ
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="preview-realtime"' \
--limit=30 --freshness=10m --format='value(timestamp,textPayload)'
run.sh の sparse-checkout 拡張 (data/estimate/
push したコードに問題があった場合は、Cloud Run Job の image を 直前の SHA に切り戻すだけで済みます。Scheduler は変更不要。
# 利用可能なタグ一覧 (Artifact Registry)
gcloud artifacts docker images list \
"${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/${IMAGE}" \
--include-tags --limit=20 \
--format='table(tags,createTime,version)' \
--sort-by=~createTime
# 指定タグへロールバック
PREV_SHA=<前リビジョンの SHORT_SHA>
gcloud run jobs deploy "$JOB_NAME" \
--region="$REGION" \
--image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/${IMAGE}:${PREV_SHA}"
:latest タグは現在のビルドを指したままなので、再ビルド前提なら
gcloud builds submit ... --substitutions=SHORT_SHA=$PREV_SHA で
過去コミット時点のイメージを再構築する手もあります。
更新で問題が出ていて切り戻しの暇もない場合は Scheduler を停止して 呼び出し自体を止めます(後述の「ロールバック」セクション参照)。
main への push で Cloud Build トリガを発火させると、infra/ や
scripts/ の変更後にイメージが自動更新されます。
gcloud builds triggers create github \
--name=preview-realtime-build \
--region="$REGION" \
--repo-name=boatracecsv.github.io \
--repo-owner=BoatraceCSV \
--branch-pattern='^main$' \
--build-config=infra/cloudbuild.yaml \
--included-files='infra/**,scripts/**'
最初のトリガ作成時に GitHub への OAuth 連携を求められます。
gcloud secrets versions add で
新バージョンを追加するだけ。Job 側は :latest 参照なので再デプロイ不要。
3 Job 全てが同じ Secret を参照しているため 1 回の更新で済む。parallelism=1 tasks=1 max-retries=0 で動く
ため同一 Scheduler 内の重複は無い。Scheduler は 3 本:
preview-realtime-daytime (*/5 8-22 * * *) /
daily-sync (30 7 * * *) /
monthly-weights (0 6 1 * *)。
Python 側もレースコードで冪等化されており、CSV 上書きは MD5 dedup される。monthly-weights 起動 (実測 10〜15 分、FeatureContext 導入後)daily-sync 起動 (実測 ~22 分)preview-realtime 5 分毎
3 Job は書き込む path がそれぞれ独立しており ファイル単位の merge conflict は
起きないが、git の non-fast-forward (= 相手が先に push して remote が進んだ)
reject は普通に起きる。とくに preview-realtime 稼働時間帯 (JST 08:00–22:59)
に monthly-weights / daily-sync の手動再実行を仕掛けると衝突しやすい。run-monthly-weights.sh: fetch + rebase + push を最大 5 回リトライ。
rebase は path 重複なしで成立する想定。詳細は本ファイル末尾の
トラブルシュート「monthly-weights の git push が rejected で失敗する」参照。run-daily-sync.sh: commit-index ステップは現状 bare push のまま run_step
で握り潰す設計。push 失敗時は当日の index が main に反映されないが、
翌朝の daily-sync が同じ index を再生成して上書きする。boatrace.git_operations.push) は fetch + rebase + push 内蔵。
preview-realtime はこれを使うため自前リトライは不要。gcloud run jobs execute monthly-weights --wait で
追走する。.github/workflows/preview-realtime.yml
は workflow_dispatch のみ残してあるので、Cloud Run 側に障害が出たら
GitHub UI から手動で 1 回起動できる。
daily-sync / monthly-weights は移行完了後 .github/workflows/*.yml を
それぞれ削除予定 (Open Question 3 で workflow_dispatch UI も廃止と決定)。
代替として gcloud run jobs execute <JOB> --region="$REGION" ... で
開発者端末から起動する:
--update-env-vars=RUN_DATE=2026-05-01--update-env-vars=TARGET_MONTH=2026-03Scheduler を一時停止すれば Cloud Run 側は完全停止します:
# preview-realtime
gcloud scheduler jobs pause preview-realtime-daytime --location="$REGION"
# daily-sync
gcloud scheduler jobs pause daily-sync --location="$REGION"
# monthly-weights
gcloud scheduler jobs pause monthly-weights --location="$REGION"
GitHub Actions 側の schedule トリガを復活させたい場合:
.github/workflows/preview-realtime.yml の on.schedule
ブロックを git revert で復活.github/workflows/daily-sync.yml を
git revert で復活 (PR #3 マージ前なら schedule ブロックを git revert するだけ)deploy-job ステップ (build step 3) で失敗するBUILD FAILURE: Build step failure: build step 3
"gcr.io/google.com/cloudsdktool/cloud-sdk:slim" failed:
step exited with non-zero status: 1
実エラーは Cloud Build のログ末尾にあります:
BUILD_ID=$(gcloud builds list --limit=1 --project="$PROJECT_ID" --format='value(id)')
gcloud builds log "$BUILD_ID" --project="$PROJECT_ID" | tail -60
主な原因:
roles/run.developer が無い
→ ログに Permission 'run.jobs.create' denied 等が出る。§5-b を実行。roles/iam.serviceAccountUser が無い
→ ログに Permission 'iam.serviceaccounts.actAs' denied on service account preview-realtime-runner@...
→ §5-b の serviceAccountUser を実行。gcloud builds list --format='table(id,createTime,createdBy)' で
実際の起動 SA を確認し、その SA に対して §5-b を実行。gcloud run jobs execute が PERMISSION_DENIED を返すRunner SA の Secret アクセス権 (§4 末尾の add-iam-policy-binding) が
未付与。再度実行する。
Authentication failed (git push)PAT が失効 / 権限不足。GitHub で fine-grained PAT を再発行し
gcloud secrets versions add github-token --data-file=- で更新。
Job 側は :latest 参照なので再デプロイ不要。
[run-monthly-weights ...] Push attempt 1/5
[run-monthly-weights ...] Push rejected; remote moved during rebase. Retrying after backoff
[run-monthly-weights ...] Push attempt 2/5
...
[run-monthly-weights ...] ABORT: failed to push weights for 2026-05 after retries
[run-monthly-weights ...] FAILED (exit=1) at line 231
preview-realtime (5 分毎) と push レースを 5 回連続で負けた状態。通常は
2〜3 回のリトライで通る。連続失敗する場合の原因候補:
preview-realtime が極端に頻繁に push している (5 分間に複数 commit)
→ 一時的なら再実行で吸収。常態化していれば preview-realtime 側の
boatrace.git_operations の動作を要調査。Rebase conflict (unexpected — ...)
ログがある場合)
→ weights CSV と preview-realtime 系の path は本来 disjoint なので、
conflict が出ているなら他ジョブが data/estimate/stadium/ を触る変更が
入った可能性。git log で該当コミットを特定する。fetch failed ログが頻発)
→ 翌時間帯に手動再実行 (gcloud run jobs execute monthly-weights
--update-env-vars=TARGET_MONTH=2026-05 --wait)。リトライ上限 (現状 5 回) は infra/run-monthly-weights.sh の
push_with_rebase 関数内 max_attempts=5 で変更可能。バックオフは
sleep $((attempt * 2)) (2s, 4s, 6s, 8s, 10s) で最大 30 秒待つ。
preview_realtime_index_skipped reason=index_csv_missing がログに出るscripts/preview-realtime.py が data/estimate/<predictor_id>/YYYY/MM/DD.csv を
更新しようとして、ファイルが存在しない/sparse-checkout されていない時の
メッセージ。predictor_id はログの構造化フィールドに含まれる。原因は2系統:
daily-sync Job (Cloud Run; JST 07:30) の build-index ステップが
当日に成功しているはず。失敗していると当日の index CSV が main に
存在しない。直近の daily-sync ログを確認:
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="daily-sync"' \
--limit=80 --freshness=24h --format='value(timestamp,textPayload)'
移行完了前は GitHub Actions 側 .github/workflows/daily-sync.yml の
Run 履歴を確認(PR #3 マージ後は GHA workflow は存在しない)。
当日の実行履歴そのものが 1 件も無い場合は Job ではなく Scheduler の発火が
失敗している可能性がある →
daily-sync が当日まるごと実行されていない。
run.sh の sparse-checkout に該当 predictor_id のパスが無い
infra/run.sh の ACTIVE_PREDICTORS 配列で展開される
data/estimate/${predictor}/${TODAY_YM} が古いイメージで checkout
されない場合、ファイルが存在してもローカルから見えない状態になる。
レジストリに新規予想者を追加したのに ACTIVE_PREDICTORS を更新
し忘れたケースが典型。「## 更新手順」に従ってイメージを再ビルド +
Job を更新。daily-sync の Cloud Run Job 実行履歴に当日ぶんが 1 件も無い場合、Job ではなく
Cloud Scheduler の発火が失敗している可能性がある。Scheduler は
jobs:run API に POST するだけなので、Cloud Run Admin API が一時的に 5xx を
返すと発火自体が落ちる。retryConfig.retryCount が未設定 (= 0) だと
リトライされず、次の発火は翌日 07:30 になり当日ぶんが丸ごと欠落する。
# Scheduler 側の発火結果 (Job のログではない点に注意)
gcloud logging read \
'resource.type="cloud_scheduler_job" AND resource.labels.job_id="daily-sync"' \
--limit=10 --freshness=3d --format=json
# リトライ設定の確認 (retryCount が無ければリトライ 0 回)
gcloud scheduler jobs describe daily-sync --location="$REGION" \
--format='value(retryConfig,status,lastAttemptTime)'
失敗時は AttemptFinished に
debugInfo: "URL_UNREACHABLE-UNREACHABLE_5xx. Original HTTP response code number = 503"、
status: UNAVAILABLE (code 14) が残る。
下流への波及(2026-08-29 の実例。JST 07:30 の発火が 503 で不発):
| 症状 | 理由 |
|---|---|
data/programs/{title,race_cards}/YYYY/MM/DD.csv が repo にも GCS ミラーにも無い |
daily-sync が走っていない |
title_csv_missing → result_realtime_candidates: 0 / payout_realtime_candidates: 0 |
締切時刻が引けず結果・払戻の対象レースが 0 件 |
preview_realtime_index_skipped reason=index_csv_missing |
当日 index CSV が無い |
毎サイクル pubsub_publish_skipped reason=no_updated_races |
assemble_updated_races が race_cards を索引に使うため updatedRaces が空になる |
| fun-site (boatrace-fun.net) の更新が止まる | Pub/Sub が飛ばず fun-site-batch が 1 度も起動しない |
preview-realtime 自体は成功し続け preview 系 CSV の commit も進むため、
git の履歴だけ見ると正常に見える。コミットメッセージが
[preview ...] だけで result / payout を含まない日が続いていたら疑う。
復旧は daily-sync を手動実行するだけでよい。title/race_cards が揃えば次の
preview-realtime サイクルが trigger=daily-bootstrap で publish し、
fun-site が全ページ再ビルドする。結果・払戻も catch-up モードで当日ぶんを回収する。
gcloud run jobs execute daily-sync --region="$REGION" --wait # 実測 ~22 分
再発防止として Scheduler にリトライを設定する (「## daily-sync の追加セットアップ」 の作成コマンドにも反映済み。既存ジョブは update で後付けする):
gcloud scheduler jobs update http daily-sync --location="$REGION" \
--max-retry-attempts=3 --min-backoff=30s --max-backoff=300s
gcloud scheduler jobs update http monthly-weights --location="$REGION" \
--max-retry-attempts=3 --min-backoff=30s --max-backoff=300s
preview-realtime-daytimeにはリトライを付けていない。5 分毎に発火するので 1 回の 503 は次サイクルが自然に吸収し、リトライを足すと実行が重なる方が 害が大きいため。
run-daily-sync.sh は if: always() / continue-on-error: true 相当の
セマンティクスを bash の run_step ラッパーで再現しており、個別ステップの
失敗は STEP FAILED (rc=N): <label> ログとして残り、最終的な exit code
は 1 になる(他のステップは続行)。Cloud Logging で grep:
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="daily-sync" AND textPayload:"STEP FAILED"' \
--limit=20 --freshness=2d --format='value(timestamp,textPayload)'
典型的に boatcast.jp の朝の更新が間に合わない場合は race-card /
recent-form / motor-stats / race-title あたりが失敗する。
翌日 07:30 の再実行で吸収される設計なので、単発であれば対応不要。
連日同じステップが失敗する場合はスクリプトのリグレッションを疑う。
compute_features_for_day が NaN だらけ / 想定より少ない結果run.sh の sparse-checkout に必要な data ディレクトリ
(data/estimate/stadium/, data/programs/recent_*/, data/programs/motor_stats/ 等)
が漏れている可能性。スクリプト側で新たに参照ファイルを増やしたら
run.sh も同時に更新し、イメージを再ビルドする必要がある。
stage_files_failed (matched paths ... outside of your sparse-checkout definition)症状: サイトの直前情報がまったく更新されない。Cloud Logging に
event=stage_files_failed と The following paths and/or pathspecs matched
paths that exist outside of your sparse-checkout definition が出る。
原因: boatrace.git_operations.stage_files() は 1 回の git add <files...>
で全ファイルをまとめてステージするため、cone 外のパスが 1 つでも混ざると
git add 全体が非 0 で終了し、その回の commit がまるごと落ちる
(cone 内の他のファイルも一緒に失われる)。新しい CSV 種別を書き出すよう
スクリプトを変更したのに run.sh の sparse-checkout を更新し忘れた時に発生。
確認:
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="preview-realtime" AND jsonPayload.event="stage_files_failed"' \
--limit=20 --freshness=2d --format='value(timestamp,jsonPayload.error)'
対処: エラーメッセージに出ているパス(例
data/previews/tokuten_hayami/2026/08/13.csv)の月ディレクトリを
infra/run.sh の sparse_paths に追加し、「## 更新手順」に従って
イメージ再ビルド + Job 更新。
予防: preview-realtime.py の PREVIEW_SOURCES や新しい出力先を足す
PR では、run.sh の sparse_paths を必ず同じ PR で更新する。
tokuten_hayami の追加(2026-08-12)preview-realtime.py の PREVIEW_SOURCES に tokuten_hayami を追加
(data/previews/tokuten_hayami/<YYYY/MM>/DD.csv)gcs_publisher.py に csv_type=tokuten_hayami を追加run.sh の sparse-checkout に data/previews/tokuten_hayami/${TODAY_YM} を
追加(2026-08-13 に追加。初期実装で漏れており、preview-realtime の
git add が丸ごと失敗して同日の直前情報が一切 commit されなかった。
上記 stage_files_failed の項を参照)v9_suji の追加(2026-08-12)infra/run.sh / run-daily-sync.sh / run-monthly-weights.sh の
ACTIVE_PREDICTORS を (v1_basic v9_suji) に変更(その後 v10_kimarite を追加)run.sh / run-daily-sync.sh の sparse-checkout に
data/estimate/suji/tables(静的)と data/estimate/suji/${TODAY_YM}(日次)を
別々に追加。cone-mode で data/estimate/suji をまとめて指定すると
日次ファイルの全履歴まで checkout されるためrun-daily-sync.sh に build-suji-picks ステップを追加(build-index の後)。
commit 対象に data/estimate/suji/ を追加run-monthly-weights.sh に build_suji_table.py を追加(build_course_rate.py の隣)詳細は operations.md。
run.sh / run-daily-sync.sh の sparse-checkout に
data/estimate/kimarite/tables(静的)と data/estimate/kimarite/${TODAY_YM}(日次)run-daily-sync.sh に build-kimarite-probs ステップと commit 対象を追加run-monthly-weights.sh に build_kimarite.py(再学習)を追加gcs_publisher.py に csv_type=kimarite を追加run.sh / run-daily-sync.sh / run-monthly-weights.sh の
ACTIVE_PREDICTORS を (v1_basic v9_suji v10_kimarite) に変更run.sh / run-daily-sync.sh の sparse-checkout に
data/estimate/kimarite/picks/${TODAY_YM} を追加
(data/estimate/kimarite/${TODAY_YM} とは別パス。cone-mode の都合)run-daily-sync.sh に build-kimarite-picks ステップを追加
(build-index と build-kimarite-probs の両方の後)run-monthly-weights.sh に build_kimarite_logloss.py(A/B の主判定)を追加gcs_publisher.py に csv_type=kimarite_picks を追加cp data/estimate/stadium/weights/v1_basic/$(date +%Y-%m).csv \
data/estimate/stadium/weights/v10_kimarite/$(date +%Y-%m).csv
詳細は operations.md。
v9_suji の退役(2026-08-22)B案 v10_kimarite と穴予想スロットが重複するため退役。成績の劣化が理由ではない
(詳細は scripts/boatrace/predictors/registry.py 冒頭コメント)。
infra/run.sh / run-daily-sync.sh / run-monthly-weights.sh の
ACTIVE_PREDICTORS を (v1_basic v10_kimarite) に変更run.sh / run-daily-sync.sh の sparse-checkout から
data/estimate/suji/${TODAY_YM}(日次)だけを削除。
data/estimate/suji/tables(静的)は残す — build_kimarite_picks.py が
kimarite_table.csv(決まり手注釈)を読むため、外すと B案の買い目が落ちるrun-daily-sync.sh から build-suji-picks ステップと
commit 対象の data/estimate/suji/ を削除run-monthly-weights.sh の build_suji_table.py は残す(上記の理由)gcs_publisher.py から csv_type=suji の spec を削除preview-realtime.py の realtime 買い目ブロックは active_predictors() の
ゲートで自動的に無効化されるため、コードは残置詳細は operations.md。