0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTTP 200 を返しながら、気象庁の警報JSONは102日間止まっていた

0
Posted at

15分おきに気象庁の警報JSONを取りに行く常駐ジョブが、2026年5月28日から9月7日までの約102日間(暦日で103日)、監視対象の市区町村の気象警報を1件も検知していませんでした

ジョブは毎回 rc=0 で正常終了していました。生存監視も、失敗通知も、一度も鳴っていません。

原因は、参照していたエンドポイントが404 にならずに HTTP 200 を返し続けたことでした。本文だけが5月28日で止まっていました。

この記事は、その事故と、再発を防ぐために何を作り直したか、そして作り直した設計にまだ残っている限界の記録です。事故そのものは筆者の内部ログに基づくもので、第三者が独立に検証できるのは「旧パスがいまも5月28日のデータを返している」という点だけです(後述のコマンドで確かめられます)。

前提

個人と会社の実務を自動化するために、常駐ジョブを40本ほど動かしています。そのうちの1本が災害アラートで、気象庁の公開データを15分おきに取得し、地震・津波・気象警報を通知チャネルに流します。監視対象は一都三県と栃木県の8市区町村です。

エンジニアではないので、コードはAIに書かせています。ただし今回の話は、AIが書いたかどうかとはあまり関係がありません。「エラーが出ていないこと」を「動いていること」だと読んだ、という一点の問題です。

なお本文で扱うJSONは、気象庁のWebサイトが画面描画に使っている内部データで、仕様やSLAが公開されたAPIではありません。機械処理の形式としては、公開仕様の防災情報XMLが基準になります。気象庁自身が登録不要のPULL型でも配信していますが、これも停止・遅延がありうる非保証の提供で、気象庁は迅速かつ確実な配信が必要なら気象業務支援センターや予報業務許可事業者に問い合わせるよう案内しています。それらを主系統に置くのは筆者の設計判断です。この記事は「そういう非保証のデータ源に依存したとき、何が起きたか」の記録として読んでください。

何が起きたか

気象庁は2026年5月29日から新しい防災気象情報の運用を開始しました(前日28日13時頃から情報発表システムの切替作業)。防災気象情報と警戒レベルの関係自体は2019年から「警戒レベル相当情報」として運用されていましたが、この体系整理で河川氾濫・大雨・土砂災害・高潮の4現象について名称と警戒レベルの対応が明確化され、レベル4相当の「危険警報」が新設されました(暴風・暴風雪・大雪・波浪は警戒レベル対応外の従来体系で、注意報段階の名称は暴風が強風注意報、暴風雪が風雪注意報)。同じ切替の時期に、Webサイト内部の市町村別警報注意報JSONの参照先が、実測上 /warning/data/r8/ に変わりました(r8 は内部URLに現れる文字列で、公開された版体系ではありません。制度変更との因果も、公開資料ではなく時期と実測からの推論です)。

  • 旧: https://www.jma.go.jp/bosai/warning/data/warning/{office_code}.json
  • 新: https://www.jma.go.jp/bosai/warning/data/r8/{office_code}.json

{office_code} は都道府県コードそのものではなく、警報を発表する気象台の担当区域(府県予報区)のコードです。東京都なら 130000 ですが、北海道や沖縄県のように複数の区域に分かれる県もあり、県コード1つで県全域を監視できるとは限りません。

問題は、旧パスが消えなかったことです。HTTP 200 を返し、5月28日時点のJSONを返し続けました。 これは記事執筆時点(9月9日)でも同じで、誰でも確かめられます。

# 旧パス(東京): HTTP 200 が返り、ルートの reportDatetime は 2026-05-28T10:16:00+09:00 のまま
set -euo pipefail          # 各行を単純コマンドとして順に実行する(どれか失敗すれば止まる)。端末セッション全体も保存しておく
W="$(mktemp -d)"           # 新しい空ディレクトリで取る(古い old.json を読まない)
cd "$W"
date -u | tee old.fetched-at.txt
curl --fail-with-body -sS -D old.headers -o old.json -w '%{http_code}\n' https://www.jma.go.jp/bosai/warning/data/warning/130000.json | tee old.status.txt
jq -e -r .reportDatetime old.json
shasum -a 256 old.json | tee old.sha256.txt   # ハッシュは同一性しか示さない。取得日時・HTTPステータス・ヘッダ・コマンドを一緒にファイルへ残して初めて「あの日そうだった」の材料になる

# 新パス(東京): 配列で、要素(電文)ごとに reportDatetime を持つ。比較証拠として同じ形で残す
date -u | tee new.fetched-at.txt
curl --fail-with-body -sS -D new.headers -o new.json -w '%{http_code}\n' https://www.jma.go.jp/bosai/warning/data/r8/130000.json | tee new.status.txt
jq -e -r '.[] | [.dataTypeCode, .reportDatetime] | @tsv' new.json
shasum -a 256 new.json | tee new.sha256.txt

旧パスのJSONはルートがオブジェクトで、reportDatetime を1つ持ちます。新パスは配列で、電文ごとに reportDatetime を持ちます。旧パスのその1つの日時が5月28日で止まり、新パスの各電文は当日の日時を返している——これが凍結の観測事実です。旧パスがなぜ残っているのか(移行措置なのか、単に取り残されているのか)は気象庁の説明が無いので分かりません。

こちらのコードから見ると、こうなります。

  • リクエストは成功する(200)
  • JSONのパースも成功する
  • 想定した構造も持っている
  • 対象市区町村を引くと「警報なし」が返る

つまりすべての工程が成功し、「今日は警報が出ていない」という正常な結果が返ってくる。ジョブは rc=0 で終わり、失敗通知の対象にならず、生存監視も緑のままです。

夏を越えて、大雨の日も、警報が実際に出ていた日も、1件も鳴りませんでした。

なぜ気づけなかったのか

監視していたのが「走った証拠」だけだったからです。

  • 終了コードは0か
  • 生存報告(heartbeat)は届いているか
  • 例外は出ていないか

これらは全部グリーンでした。見ていなかったのは「働いた証拠」です。

  • 最後に警報を1件でも検知したのはいつか
  • 取得したJSONは、いつ発表されたデータなのか

後者について言えば、旧パスの reportDatetime は最初からレスポンスに入っていました。ここに「3週間更新が無ければ異常」という閾値を1つ置いていれば、今回と同じ型の停止は6月18日頃に検出できていました。前者(最後に検知した日)は、日付だけでは長い平穏と故障を区別できない補助指標ですが、閾値や通常の頻度と組み合わせれば「何日鳴っていないか」を問いの形にしてくれます。見ていなかっただけです。

「102日間鳴っていない通知」を「平穏の証拠」と読んでいたのが根本です。鳴らない警報は、平穏か故障かのどちらかで、区別する仕組みを持たなければ区別できません

誤診で1回転した話

発見した日、最初に立てた仮説は「単に警報が出ていない日が続いただけではないか」でした。これを潰すために、天気系のまとめサイトの警報ページと突き合わせようとしました。

このとき見ていた画面では、切り分けになりませんでした。

こちらが検知対象にしているのは、実装で選んだ警報・危険警報・特別警報です。注意報に加えて、現行実装では通常の波浪警報と高潮警報も対象外です(当初の設計で対象にしなかった範囲で、対象に加えるかは別途判断します)。その日に開いた一覧画面では、市区町村単位で「これは警報なのか注意報なのか」「発表時刻はいつか」を、こちらのログと同じ粒度で突き合わせることができず、「大雨注意報が出ている」という情報からは、こちらが鳴っていないのが正常なのか故障なのか判定できませんでした。サイト側はレベル別に区別して表示しています。切り分けに必要な粒度を、要約された画面に求めたのが間違いでした。

結局、旧パスと新パスを叩いて中身を比較するまで、凍結だと確定できませんでした。この2本は同じ気象庁が出しているものなので、独立した対照群ではありません。「旧パスの日時が止まり、新パスは当日を返す」という差分が、凍結の証拠になっただけです。

教訓としてはありふれていますが、実際に踏むとコストが高い。**故障の切り分けには、対象と同じ粒度・同じ時刻軸で比較できる照合材料が要る。**可能なら提供経路の異なる系統で。要約された画面は、その粒度を持っていないことがあります。

何を変えたか

4点あります。順番に「なぜその設計か」を書きます。

1. エンドポイントを r8 に移した

ENDPOINT = "https://www.jma.go.jp/bosai/warning/data/r8/{office_code}.json"

これは移行対応そのもので、再発防止ではありません。**次に体系が変わったときにまた同じことが起きます。**だから残りの3点が本体です。

なお、現行パスは警報ページのHTMLから確認できます(保証された仕様ではなく、あくまで「いま画面が何を読んでいるか」の確認です。9月9日時点では warning/data/r8/ が4か所に現れます)。

set -o pipefail; curl --fail-with-body -sS https://www.jma.go.jp/bosai/warning/ | grep -Eo 'warning/data/r[0-9]+/' | sort | uniq -c

2. 構造を検証する

r8 の配列は、9月7日〜9日の観測では電文種別ごとに1要素で、利用する範囲は共通の形をしていました(dataTypeCodeVPWW55 大雨 / VPWW56 土砂 / VPWW57 高潮 / VPWW58 暴風(暴風雪を含む) / VPWW59 波浪 / VPWW60 大雪 / VPWW61 その他注意報。名称は気象庁の技術情報第634号)。どの種別が含まれるかは府県予報区と時期で変わります。たとえば9月9日の東京都は 55/56/58/59/61 の5電文で、高潮と雪の電文はありません。公開仕様ではないので、この形を不変条件とは扱わず、実行時に検証します。

実装で検査しているのは次です。

  • ルートが配列で、空でないこと
  • 各要素が dict で、dataTypeCode(文字列)と warning.class20Items(list)を持つこと
  • 各区域が dict で、areaCode が7桁の数字、kinds が list であること(class20Items の単位は二次細分区域で、概ね市町村ですが分割されている市もあります)
  • kind が dict で、status が文字列であること。発表中の kind は code が2桁の数字であること
  • 監視対象の区域コードが、(後述の隔離を除いた)いずれかの電文の class20Items に存在すること(後述。9月9日に追加。「いずれか」なので、ある警報を担当する電文からだけ消えた場合は通ってしまう検査です)

1電文でも構造が壊れていれば府県予報区全体を「観測不能」とみなします。9月7日〜9日に観測した範囲では実装が使う構造が全電文で共通していたので、1電文だけ形が違う場合に仕様変更の可能性を排除できず、その場合は正常に見える残りの電文も信用しない方が安全だ、と判断したからです(確率的な根拠があるわけではありません)。ただしこれは片側にしか安全ではありません。通知対象外の VPWW61(その他注意報)だけが壊れた場合でも、正常な VPWW55(大雨)の新しい警報まで捨てることになります。電文種別ごとに「正常/異常/不在」を持ち、正常な種別からは新規発表を通知しつつ、異常な種別に載っていた警報の解除判定だけを保留すれば両立できます。後述の「種別単位の鮮度」ではこの形を採ったので、構造異常にも広げるのが次の課題です(末尾の限界に含めています)。

検査していないものも書いておきます。dataTypeCode の値域、kinds[].code の値域、status の文字列とコードの整合、電文種別と警報コードの組合せ、kinds が空配列の区域(正常な「警報なし」は {"status":"発表警報・注意報はなし"} の単一要素ですが、空配列も「警報なし」として通ります)、同一電文内の areaCode の重複、同一区域内の code の重複(VPWW55 に土砂災害のコードが入っていても通します。dataTypeCode の重複は、この記事の検査中に「古い重複要素の警報が、新しい同種要素の日時で鮮度をすり抜ける」と指摘され、仕様変更の疑いとして観測不能に倒すようにしました)。これは今回の事故と同じ形の穴です。未知の警報コードが追加されても構造検証は通り、「対象外」として黙って無視されます。未知の状態値は逆で、実装が解除側として認識する "解除""発表警報・注意報はなし"(完全一致)以外を発表中側に倒すので、通知対象のコードが伴えば誤通知になりえます(この2値と、code を持たない正常な「警報なし」の要素はテストで固定しています)。既知のコード表(気象庁の技術資料。使う版と取得日を控える)と、許容する status とコードの組合せを突き合わせ、未知の値は「警報なし」でも「発表中」でもなく「仕様変更」として鳴らすのが本筋で、これは未着手です。構造検証は「形が想定どおりか」を見るだけで、正常な形の古いJSONは検出できない——それが次の項目です。

最後の項目(監視対象の市町村コードの存在)は、この記事の初稿を別系統のモデルに検査させて指摘された穴です。同じ2026年5月の体系整理で、横浜市・相模原市・長野市など8市の発表区域が分割されています。監視対象のコードが配列から消えたとき、素朴な実装は「その市に警報なし」と読みます。区域変更で無音になる型で、今回の事故と同じ形です。存在しなければ「警報なし」ではなく「観測不能」に倒すようにしました(監視対象の8市区町村は今回の分割対象ではありませんでした。設定時に公式の区域マスターと突き合わせる検査は入れていません)。

3. 鮮度を確認する(← 今回と同型の長期凍結を遅延検知する本体)

構造だけ見ていても、今回の事故は防げません。旧パスが返していたJSONは、旧実装が期待していた旧形式の構造を満たしていたからです。古いだけでした。

そこで、配列内の reportDatetime を見て、閾値より古ければ「鮮度を確認できない」として ValueError にします。

STALE_AFTER = timedelta(days=21)           # 配列内の最新 reportDatetime がこれ以上古ければ観測不能
STALE_AFTER_PER_TYPE = timedelta(days=45)  # 種別単位でこれ以上古い電文は隔離(その種別だけ処理から外す)
FUTURE_TOLERANCE = timedelta(days=1)       # 1日以内の未来は許容(暫定)。超えたら壊れた値=その府県予報区を観測不能に倒す

正直に書くと、reportDatetime は(9月7日〜9日の実測と、公開仕様であるXML側の発表時刻との照合から)各電文の発表時刻に対応していると解釈できる値で、内部JSON上の契約ではありませんし、上流の死活を示す値でもありません。警報注意報の電文は随時発表なので、古い日時だけでは「凍結」と「長く発表が無かった」を区別できません。3週間という値も、契約・仕様・観測分布のどれかに基づくものではなく、運用上の暫定値です。したがって、今回と同じ型の故障(全電文の日時が正直なまま止まる)なら約21日+最大15分で検出できますが、裏返せば3週間は気づけません。そして、この閾値が上限を与えるのはその型だけです。特定の種別だけが配列から消える、本文だけが壊れて日時は更新され続ける、といった型には上限を与えません。検出できるのも「ジョブが15分間隔で動き続け、時計と失敗通知が正常」という前提の下で、凍結そのものも、その間の見逃しも防いではいません。なお公式XMLには、全国分の警報・注意報の発表状況を高頻度で定時通報する VPWS50(警戒・注意事項集約定時通報)があり、生成時刻の鮮度監視には随時発表の VPWW5561 より向いています。今回それを主系統や照合系統にしなかったのは、非保証のWeb内部JSONに依存した既存実装を最小の変更で立て直すことを優先したからで、XML系統への移行は別の課題です。

初稿では最新値 max() だけを見ていましたが、それだと1種別が更新されていれば他種別の停止が隠れます。9月9日に、種別ごとの日時を全部読む形に変え、存在する電文の日時が欠落・不正・1日超の未来なら観測不能に倒すようにしました。一方で「種別単位で45日以上更新されていない」は、府県予報区全体を観測不能にはせず、その種別だけを処理から隔離します(その種別の警報は、最終確認から45分を超えるまで通知も解除もせず保留する。超えた後は、種別消失や取得失敗と同じ安全弁が追跡状態と重複抑止を破棄し、復旧時に継続中の警報を再通知することがある。他の種別は通常どおり処理する)。警報注意報の電文は随時発表で、高潮や大雪のように季節性の強い種別は平常時に長く更新されないことがありえ、府県予報区全体を止めると他種別の新しい警報まで止めてしまうからです。隔離の出入りは状態に記録して、変わったときだけ WARNING ログを出します(人への通知ではありません)。45日に根拠はありません。気象庁が公開しているのは、これらの電文が「随時」発表であることまでで、最大更新間隔は保証されていません。したがって、1種別だけが本当に凍結しても、隔離への遷移時のログ以外は鳴らない、という盲点を受け入れています。また「存在する電文」を見る検査なので、種別が配列から丸ごと消えても検出できません。既に追跡中だった警報は45分の閾値を過ぎた後の実行で状態を破棄しますが、消失後に新しく発表された警報は観測も検知もできず、検出時間の上限もありません。根本対策には種別の「正常/古い/不在」の三状態管理と、期待される種別表や別系統(XML)との照合が要ります。

未来日時を「除く」のではなく「観測不能に倒す」のは、除くと別の正常な日時によって全体が新鮮と判定され、壊れた電文の本文だけが処理されてしまうからです(初稿では除いていました)。なお1日以内の未来は現在の実装では許容しており、この幅にも根拠はありません(時計ずれとして説明できる分単位に縮めるのが筋です)。

4. 失敗を鳴らす(府県予報区単位まで)。倒し方は2段、通知は別経路

「取得に失敗したときどう振る舞うか」は、1つの答えでは足りません

状況 状態の扱い 通知
一部の府県予報区が取得失敗・構造異常・鮮度不明・対象区域の消失 その区域は「今回観測できなかった」扱い。アクティブな警報集合は保持し、最終確認から45分を超えた後の最初の実行で重複抑止と追跡の両方を破棄(通常の15分間隔なら45〜60分後。ジョブ自体が止まれば上限は無い) 初稿の時点ではログのみ。9月9日に、連続4巡(障害開始から45〜60分)観測できなければ rc=1 で失敗通知に乗せるよう変更(通常時。連続回数を持つ状態ファイルの消失・破損・不正キーは state 自体の異常として即時に鳴らし、その巡に失敗した県は即座に閾値到達として扱う)
全区域で観測できない 同上 rc=1 → ジョブの失敗通知(共通の runner が30分のクールダウン後の次回実行で再通知し、復旧時に復旧通知を1回出す)

保持は「未観測を解除と誤認して重複抑止を早く解放しない」点では保守的ですが、新しい警報の検知については危険側に倒れています。だから状態の扱いと障害の通知は分けて、観測できないこと自体を、データ処理とは別の監視レイヤで人に知らせる必要があります(ただしホストや通知基盤は共有しているので、独立した外形監視ではありません)。現状これが成立しているのは府県予報区単位の連続失敗までで、種別単位の観測不能は WARNING ログを出すだけで人には届きません(未実装)。初稿ではこれが「全区域で失敗したとき」しかありませんでした。監視対象が1府県予報区だけなら全区域障害と同じですが、複数の府県予報区を見ていると1区だけの凍結は永久に鳴りません。別系統のレビューで指摘され、連続失敗の回数を区域ごとに数えて一定回数で鳴らす形に変えました(単発のHTTP失敗では鳴らさず、復旧で回数は消えます)。

もう1つ、補助的な見張りとして、警報ページのHTMLが参照している warning/data/rN/ のパスを毎巡読み、r8 が無くなっていれば rc=1 にしました。HTMLの部分文字列検索なので、判定は2段です。warning/data/rN/ の形は検出できるのに r8 が無い(r9 等だけ)場合は「不一致」で、2巡連続で初めて鳴らし、一度鳴らしたら明示的に r8 を見つけるまで rc=1 を維持します(次の巡でページ取得に失敗しただけで「復旧」と誤認しないため。逆に、1回の不一致の直後にページ取得失敗が来た場合は連続とは数えません)。ページの取得失敗や rN の形自体を検出できない場合は「不明」で、単発では鳴らしませんが、r8 を最後に確認してから24時間経てば、不一致と不明が交互に来ていても鳴らします(見張り自体が死んでいるか、ページ構造が変わった疑い)。例外として、見張りの状態ファイルを失った直後に r8 を確認できなければ即時に鳴らし、明示的な確認まで維持します(状態を失ったまま初回扱いにすると、鳴っていた警告が「復旧」に化けるため)。これを「直接の再発防止」と呼ばないのは、部分文字列の検索だからです。コメントや未使用のコードに r8 が残っていれば、実際の参照先が r9 でも通ります。主系統はあくまで鮮度判定で、この見張りは早期警戒の補助です。

副作用の設計

移行に伴って、解除の扱いも作り直す必要がありました。

先に前提を書くと、このジョブは「解除」を通知しません。通常時に通知するのは新規発表とレベルの変化だけで(状態を破棄した後の復旧時には、継続中の同一警報を再通知する例外があります)、解除の観測は「同じ警報が次に発表されたとき、重複として抑止せずにもう一度通知するため」の内部状態のリセットにだけ使います。

**配列に含まれる電文種別は、府県予報区と時期で変わります。**そこで素朴に「今回のJSONに無い警報は解除された」と判定すると、電文自体が来ていないだけの現象を、片っ端から解除扱いにしてしまいます

対応はこうしました。

  • 各警報が実際にどの電文種別に載っていたかを状態として保存する
  • 解除の観測は、今回の配列に存在した電文種別の警報だけに対して行う
  • 電文が無い現象の警報は保留する(確認時刻を更新しない)
  • 保留が45分を超えた後の最初の実行で、重複抑止の状態とアクティブの追跡の両方を破棄する(15分間隔なら45〜60分後。ジョブが止まっていれば上限は無い)。解除の通知は出さない。その電文種別が再び現れた取得時に、継続中の警報は新規扱いで再通知されることがある。欠落と復帰が繰り返されれば重複も繰り返しうる

45分という値に根拠があるわけではありません。「取得できない窓の中で解除→再発表が完結していた場合に、恒久的に通知が止まる」のを防ぐために置いた安全弁で、副作用は重複通知の側に出ます。見逃しより重複を選んだ、という判断です。ただし成り立つのは45分を超えた場合だけで、45分未満の窓の中で解除→同じ警報の再発表が完結すると、再発表は重複として抑止されます。短い窓の解除→再発表は識別できない、という限界です。

移行で仕様そのものが変わった点も2つあります(いずれもこの市町村別警報JSONの中での話です)。

  • **市町村ごとの洪水警報・注意報は廃止されました。**洪水予報河川は指定河川洪水予報、水位周知河川は水位到達情報が河川別の主な情報になります。一方、市町村別の大雨警報・注意報は、内水氾濫に加えて洪水予報河川以外の外水氾濫も対象にし、水位周知河川については当面、大雨警報・注意報でも扱います(国土交通省・気象庁「河川氾濫・大雨に関する情報の改善」2026年4月更新版)。河川別の情報は別系統なので、このJSONだけでは河川氾濫の情報は網羅できません
  • 従来の土砂災害警戒情報は、このJSONの警報コードではなく別電文(VXWW50)として提供されていました。新体系では主たる体系が VPWW56 に再編され(VXWW50 自体は移行措置として、新電文の運用開始後2年程度の並行配信が予定されています)、その kinds[].code ではレベル4の土砂災害危険警報を 49 で表します。旧実装は警報種別コードの表に 50=土砂災害警戒情報という行を持っていましたが、これは別電文 VXWW50 の末尾を警報種別コードと取り違えた設定で、旧JSONの warnings[].code50 は現れないので到達不能でした。つまり旧実装は土砂災害警戒情報を最初から検知できていません。新体系では VPWW56kinds[].code=49(レベル4土砂災害危険警報)を新たに対象にしました。50 から 49 への改名ではありません(電文種別のコードと、電文の中の警報種別コードは別の名前空間です)

ついでに旧実装のコード表を突き合わせたところ、コード表に個別の誤りが複数ありました。旧実装は 06 を暴風雪警報、07 を大雪警報、08 を波浪警報、14 を高潮警報として持っていましたが、気象庁の表では 06 大雪警報、07 波浪警報、08 高潮警報、14 雷注意報です(特別警報も、旧実装は 36/37/38/39 を暴風雪・大雪・波浪・高潮の特別警報としていましたが、旧公式体系では 32/36/37/38 がそれぞれ暴風雪・大雪・波浪・高潮で、39 は旧体系の対応コードではありませんでした)。通知対象には 06/07/36/37 が入っていたので、実動作としては、大雪警報を「暴風雪警報」、波浪警報を「大雪警報」、大雪特別警報を「暴風雪特別警報」、波浪特別警報を「大雪特別警報」として通知する一方、暴風雪警報 02 と暴風雪特別警報 32 は表に無く見逃す構成でした(08/14/38/39 は表にはありましたが通知対象外でした)。5月28日以降は上流が凍結していたので発報自体がありませんが、それ以前に誤通知・見逃しが実際に起きたかは保存ログで別途確認が必要です。移行のたびにコード表を照合し直さないと、こういうズレは静かに残ります。この記事の検査でも、暴風雪を「暴風か大雪か不明」として両方の電文を要求していた対応表が見つかり、公式資料(暴風(雪)=VPWW58、大雪=VPWW60)に合わせて直しました。9月7日時点の8都府県の実データで kinds[].code と危険度の対応を確認しましたが、1時点のサンプルで、まれな危険警報・特別警報や解除の遷移まで網羅したものではありません。公式のコード表を正として、保存したフィクスチャでテストするのが本筋です。

残っている限界

この記事は、書いたのとは別系統のモデル(Codex)に技術的な事実と論理の検査をさせ、8回改稿しています。補助的な手段にすぎませんが、指摘のうち設計に関わるものはそのまま残しておきます。

  • 鮮度の判定は暫定値に依存している(全体3週間・種別45日)。分・時間単位で止まっていることに気づきたいなら、取得成功率・同一ペイロードの継続時間・別情報源との照合が要る
  • 失敗通知は30分のクールダウン後に再送される。15分間隔のジョブなら恒久障害で1日最大48通で、抑止と見逃しのバランスは共通の runner 側の設計に依存している
  • データ源が非保証のWeb内部JSONである。ページの参照パスを見張る対策は、r8 の文字列が残る構造変更を検出できず、新しい参照先も特定できない(文字列自体が消えた状態は24時間後に鳴る)
  • 解除の観測は45分の安全弁に頼っている。重複通知の側に倒してあるが、頻度は上流の電文の出方に依存する
  • 1電文の構造異常で府県予報区全体を捨てる。正常な種別の新規警報も捨てる片側安全。種別ごとの正常/異常/不在の管理に広げるのが次の課題
  • 未知のコードは「対象外」として黙って通り、未知の状態値は「発表中」側に倒れる。コード表・状態値との突合も、電文種別と警報コードの意味的整合も、kinds の空配列や areaCodecode の重複も未検査で、今回と同じ形の穴が残っている
  • 種別が配列から丸ごと消えた場合は種別鮮度で検出できない。1種別だけの凍結は隔離時のログ以外で鳴らない
  • 事故の記録は筆者の内部ログに基づく。第三者が独立に検証できるのは、旧パスがいまも5月28日のデータを返していることだけ。保存したフィクスチャやテストコードは公開していない

「直した」と書くのは簡単ですが、直した設計の穴を書けるかどうかが、次の事故までの距離を決めると思っています。

検証について

修正そのものは半日で書けましたが、レビューに時間をかけました。書いたのとは別系統のモデルにコードを23回(9月7日の移行対応で13回、9日の改修で10回)レビューさせ、この記事も8回検査させています。理由は、同じモデルに自分の書いたコードを採点させると、同じ盲点が相関して残り、誤って通すおそれがあるからです。ただしモデルが違えば独立性が保証されるわけではなく、回数も品質の指標にはなりません。最終的に効いたのは、実データを縮約したフィクスチャによる単体テストと、古い日時のJSONを流して「観測不能」に倒れることを確かめる再現テストでした(リポジトリは非公開で、縮約フィクスチャもテストコードも公開していません。実レスポンスの再投入と102日間の推移も内部記録です)。

デプロイ後、初回実行で3件の警報を検知して通知が飛びました。約102日ぶりです。

一般化 — 新しい常駐ジョブに当てる問診票

同じ型の事故を繰り返さないために、係を1つ増やすたびにこれを当てています。

  • 走った証拠」(終了コード・生存報告)と「働いた証拠」(成果物・検知件数)を、区別して監視しているか
  • 最後に新規を発見した日」に相当する物差しと、それに当てる閾値(通常の頻度か、独立した正解データ)を持っているか。日付だけでは沈黙が平穏か故障か判定できない
  • 取得したデータの鮮度を、成功条件に含めているか。構造の検証だけでは、古い正常な形のデータを通してしまう
  • 鳴らない通知に、保存済みの実データ(フィクスチャ)を隔離環境で流して、鳴ることを確かめたか(実利用者の防災チャネルへ、訓練表示の無い模擬警報を送らない。配送経路まで検査したいなら、専用のテスト宛先に「訓練/TEST」表示付きのカナリアを送る)
  • 外部依存(エンドポイント・区域コード・認証の寿命・仕様変更)が予告なく変わった日、何が同時に止まるかを言えるか
  • 取得失敗を「現状維持」に倒すのか「異常として鳴らす」のか、レイヤごとに決めてあるか。そして「現状維持」に倒した失敗が、誰にも知らされない状態になっていないか

一番効いたのは3番目です。**HTTP 200 はリクエストが成功したことを示しますが、本文の鮮度や、業務上正しいデータであることまでは保証しません。**そこを区別する物差しを持っていなかった3か月分が、今回の事故でした。


出典


※ この記事は Zenn に投稿した同名記事(https://zenn.dev/masamitsu_sera/articles/c0c71dbe82ea79 )の転載です。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?