年払い 年払いなら、すべての有料プランで2か月分が無料。 プランを見る
サインイン 無料で始める

利用状況ページの数字は高く出ていて、お金は一円も動いていません

先に、読み進めるうちに最も失われやすい部分から書きます。お金は取られていません。ウォレットが二重に引き落とされたことも、カードに請求が立ったことも、行われなかった作業にサブスクリプションの割当が使われたこともありません。 壊れていたのは画面上の数字です。一部のアカウントで、ご自身のアカウントページの利用内訳が高く表示されていました。計測した中で最悪のアカウントでは、要約対象である台帳の2.4倍でした。台帳そのものは終始正しいままでした。…

先に、読み進めるうちに最も失われやすい部分から書きます。お金は取られていません。ウォレットが二重に引き落とされたことも、カードに請求が立ったことも、行われなかった作業にサブスクリプションの割当が使われたこともありません。

壊れていたのは画面上の数字です。一部のアカウントで、ご自身のアカウントページの利用内訳が高く表示されていました。計測した中で最悪のアカウントでは、要約対象である台帳の2.4倍でした。台帳そのものは終始正しいままでした。まったく別の場所から読まれる月間上限も同じです。これは表示上の欠陥で、すでに修正済みです。原因は以下のとおりです。

誰も書いていないものを探していたキー

あなたの操作をきっかけに起きる処理——日次の利用サマリーの更新、紹介報酬の付与、通知の配信——は、あなたのリクエストの中では行われません。リクエストはメッセージを書き、バックグラウンドのコンシューマーが少し後にそれを拾います。遅い副作用が速いページを遅くしないのは、この仕組みのおかげです。

この構成の危険は、同じ処理を二度行ってしまうことです。ブローカーは不具合のあとで同じメッセージを平然と再配信するからです。防御は受信箱です。処理の前に、コンシューマーはそのメッセージのキーを持つ行を書いてメッセージを確保し、ユニークインデックスが二度目の確保を不可能にします。

私たちの実装は組で確保していました。メッセージが属するテナントと、メッセージキーの組です。そしてその組を、メッセージのエンベロープが運ぶテナントID——これは値が入っています——と突き合わせて探していました。

確保行のほうには、テナントIDがまったく入っていませんでした。すべて空で、最初からそうでした。エンティティ自身のドキュメントには、フレームワークが挿入時にこれを刻印する、とそのままの言葉で書かれていました。実際には一度も刻印されていませんでした。発見した当日に本番で計測した結果、受信箱の3,344行はすべて空、一方で生成側の1,451行のうち1,174行には実在するテナントが入っていました。読み取り側が疑われなかった理由は、まさにこれです。

つまり検索は(テナント、キー)を求め、保存されていた行は(なし、キー)で、一致することは決してありませんでした。

何も失敗しなかった。だから何週間も続いた

決して一致しない検索は、最初の一回でエラーを出しそうなものに聞こえます。実際は逆でした。

確保処理は「このメッセージは新しい」と判断しました。続く挿入がユニークインデックスに当たり、重複として拒否されました——行はすでに存在していたので、これは正しい動作です。フォールバックの分岐がその拒否を受け取り、好意的に解釈しました。別の配信がこれを確保したのだろう、ここですることはない、と。エンベロープは「未処理」の状態のまま残り、未処理である以上、次の配信がそれを拾ってもう一度処理しました。

すべてのコンシューマーが、およそ45秒ごとに自分の滞留分を無期限に再処理していました。26個がそうしていました。ある本番の行は到着から333時間後に完了しており、その大半の時間、何度も何度も完了し続けていました。

実際に壊れていたもの

その26個のコンシューマーのうち、4個は上書きではなく積み上げます。その4個が利用状況ページの読む課金プロジェクションであり、問題が表面化したのもそこでした。

この4個には、まさにこの事態に対する明示的な保護がありました。8月に、単一のイベントがゲージに9回適用された事故のあとに書かれたものです。この保護は、処理する前に行を成功として印を付け、再配信がその印を見つけて停止するようにします。印は毎回正しく書かれていました。ところが次の確保処理が、同じ壊れたキーでその印を探し、見つけられませんでした。この保証は、リリースされた日から空だったのです。

これが触れなかったものは正確に列挙する価値があります。「課金プロジェクション」という言葉は不安を誘いますが、事実はそうではないからです。

  • 台帳は正しかった。 すべてのAI呼び出しは、自身の価格スナップショットを持つ課金レコードをちょうど1件書きます。この経路にはそれを書くものはありません。
  • ウォレットは動いていない。 残高は台帳から算出され、サマリーからは決して算出されません。
  • 上限も動いていない。 予算ガードは別のカウンターを読みます。今月いくら使ったかについての唯一の真実はそちらです。水増しされた数字のせいで早く止められた人はいませんし、通るべきでない人が通ったこともありません。

影響を受けたサマリーを読むのは、ご自身のアカウントページの利用履歴だけです。合計額と、その内訳です。影響範囲はこれがすべてで、その内容は「高く表示されていた」ことでした。

誰も見ていなかったもう半分

同じ壊れた検索が、もう一か所にありました。メッセージが失敗したときに走る経路です。そこでは更新すべき行が決して見つからず、そのすぐ下には「実際には到達しない」というコメントの付いた分岐がありました——失敗のたびに「この失敗は終端である」を返す分岐です。

そのためリトライ回数は一度も進まず、毒メッセージがデッドレターキューに到達することもありませんでした。エラー処理の層がまるごと無効化されていたのです。読んだ全員に「ここは確認する必要がない」と告げるコメントの陰で。

修正

両方の検索は、コンシューマーグループ内のメッセージキーを使うようになりました。ユニークインデックスがもともと強制しているのはこれです。メッセージキーは、それを生み出した対象のIDから導出されるため、構造上テナントをまたいで衝突しません。

刻印されると主張していたドキュメントは、削除ではなく訂正しました。あの一文こそが罠であり、削除してしまえば「ついに誰かが確認した」と示すものが何も残りません。

9件のテストがこの挙動を固定しています。うち6件は旧コードで失敗します。旧コードを戻して確認済みです。

そのあとの後始末

水増しされた行は自己修復します。ワーカーが1時間ごとに、2日間のウィンドウにわたって日次サマリーを台帳から再構築します。これは最初から動いていて、そして負けていました。45秒ごとに再適用するコンシューマーに対して1時間に1回では、公平なレースになりません。

コンシューマーが再実行しなくなった今、ワーカーが勝ちます。まだそのウィンドウ内にあった2日分は、修正が本番に入った時点で台帳から再構築されました。それより前の2日分はすでにウィンドウの外に出ており、そこに残っているのは私たち自身のアカウント1件と、別の箇所の無視できる差分だけです。そのため、それを追うために本番のウィンドウを広げるのではなく、そのままにしました。掃除が完全だったかのように匂わせるよりは、こう書き残すほうがよいと考えました。

ここから得たこと

重複排除キーは、コードが「書かれたはずだ」と信じているものではなく、インデックスが実際に強制しているものを映さなければなりません。このキーの二つの半分は別々のフィールドから供給されていて、どちらも相手と突き合わせて計測されたことがありませんでした。いまは両方とも計測されており、それを証明するテストは、間違っていたバージョンに対して赤くなります。

共有 LinkedIn X メール
ご自身の仕事で試してみてくださいどの返信にもかかった費用が表示されます。無料プランから始められます。 無料で始める
ノイズではなく、製品アップデートを 新モデル、新機能、changelogのハイライト——本当に何かがリリースされたときだけ、短いメールでお届けします。
登録すると、ToRun から製品アップデートのメールを受け取ることに同意したことになります。配信はいつでも停止できます。 プライバシーポリシー