古い依存ファイルが1つあるだけで、バックエンドの関数が丸ごと消える
- 1 時間前
- 読了時間: 4分
テスト注文を作る webMethod が Unable to handle the request で落ちた。この関数は段階ごとの結果を steps という配列に積んで返すように作ってあり、途中で失敗しても、どこまで進んだかは返ってくるはずだった。ところが steps は1件も返らなかった。
0件ということは、1段目に入る前に落ちている。つまり犯人は処理の中ではなく、その外側にある。候補は2つで、本体を囲む関数の外か、モジュールの読み込みそのものだった。
トップレベルの elevate() が巻き添えを作る
この webMethod は、注文のキャンセルを担当する別の .web.js を import していた。そちらがモジュールの先頭で権限昇格を3つ作っていた。
const elevatedSearchOrders = elevate(ecomOrders.searchOrders); // 評価時に走るelevate() は関数を受け取って、昇格した関数を返す。渡した値が undefined なら、そこで失敗する。eCom の API 名は版によって変わることがあるので、3つのうち1つでも名前が変わっていれば、この行で投げる。
投げる位置がモジュールの直下だと、失敗するのはその関数ではなくモジュール全体である。読み込みに失敗したモジュールは、そのファイルを import しているファイルまで道連れにする。実際、落ちていたのは昇格を作っている側の関数ではなく、それを import している側の .web.js だった。使ってすらいない機能のせいで、呼んだ関数が読み込みの段階で消えていたことになる。
症状の側から見ると、これは「どこで落ちたか分からない汎用エラー」にしかならない。Velo はバックエンドの例外文面をフロントエンドへ渡さないので、画面に出るのは Unable to handle the request だけである。段階を返す仕組みを用意していても、その仕組みごと読み込まれていない。
同じ落とし穴が、次は計算キーで出た
数日後、集計を担当する別のモジュールでも、同じ形の問題に行き当たった。管理者向けのダッシュボードから呼ぶ webMethod が5つとも消えたのである。直接の原因は別の話(webMethod() の第2引数の書き方)だったが、その調査の途中で、このファイルが依存に対してとても脆い作りになっていることが分かった。モジュールの評価時に外部へ触る箇所を2種類持っていた。
箇所 | 触る先 | 依存が古いと起きること |
計算キーで作った定数 { [MAIL_KIND.REMINDER]: … } | public/constants.js | MAIL_KIND が届かないと即 TypeError になり、5関数が全滅する |
トップレベルの elevated(() => …) が2つ | backend/dataAccess.js | elevated が無いと is not a function になり、5関数が全滅する |
計算キーは、オブジェクトリテラルに見えて実は import した値を読んでいる。undefined.REMINDER を評価するので、届いていなければその場で投げる。elevate() の直呼びと理屈は同じで、書き方が違うだけである。
皮肉なことに、dataAccess.js の elevated() には「モジュール読み込み時に elevate() を呼ばない。呼ばれた時点で解決し、無ければその関数の失敗として扱う」という注意書きが最初から書いてあった。同じ落とし穴を、その関数を呼ぶ側で踏んでいた。
壊れ方を変える
依存が古くなること自体は防げない。手で貼り付けて運用している以上、サイト上のどれかが1つ古い状態は、いつでも起こりうる。変えられるのは、そのときの壊れ方のほうである。
評価時に触っていた2か所を、呼ばれた時点で解決する形へ移した。定数は関数にした。昇格は遅延解決のラッパにした。
function lazyElevated(getFn, label) {
let cached = null;
return async (...args) => {
if (!cached) {
if (typeof elevated !== 'function') {
throw new Error(
`${label}: backend/dataAccess.js の elevated() が見つかりません` +
'(サイト上の backend/dataAccess.js が古い可能性があります)。'
);
}
cached = elevated(getFn, label);
}
return cached(...args);
};
}これで、定数やデータ取得のモジュールが古くても、集計と CSV の書き出しは動く。失敗するのは発送処理とメール送信だけで、しかも「どのファイルが古い可能性があるか」を名指しした文面で失敗する。全滅と部分的な失敗の差は、そのまま診断のしやすさの差になる。
結果として、モジュールの直下に残るのはリテラルとクロージャの生成だけになった。並列数の上限、ID をまとめる件数、エラーを文字列にする小さな関数、そして先ほどの遅延ラッパが2つ。どれも import した値を読まない。
評価時にやってよいことの線引き
Velo のモジュールは、最初に呼ばれた時点で1度だけ評価される。その1回に失敗すると、そのファイルの export は存在しないものとして扱われ、import 元まで同じ状態になる。だから評価時のコードは、依存の状態に関わらず必ず成功する範囲に限る。
リテラル、関数宣言、クロージャの生成は安全である
import した値を読む計算キーは安全ではない。関数にして、呼ばれたときに読む
elevate() や、それに類する「関数を受け取る関数」の直呼びは安全ではない。遅延解決にする
import した定数を使った即時計算は、失敗すら見えないので最も危ない(これは別の記事で扱う題材になる)
この線引きは、Velo に限った話ではないように見えるかもしれない。ただ、手で貼り付けて反映する運用と、ビルドエラーが出ないまま export が消える挙動が組み合わさると、影響の範囲が普通のプロジェクトより一段広くなる。依存の1ファイルが古いという、ありふれた状態が、機能の全滅として出てくる。
コメント