【ITニュース解説】We nearly charged our own buyers twice for rows they'd already paid for
2026年09月23日に「Dev.to」が公開したITニュース「We nearly charged our own buyers twice for rows they'd already paid for」について初心者にもわかりやすく解説しています。
ITニュース概要
データ収集システムが既配信データIDを記録し重複課金を防ぐ仕組みにバグがあった。記録IDの上限を超えると古いIDが警告なく削除され、システムが「新しい」と誤認。結果、顧客にデータが重複配信され、二重請求が発生していた。現在は、この問題を可視化し早期発見できるよう修正された。
ITニュース解説
システムが提供するサービスの裏側では、データの効率的な管理と正確な請求が非常に重要になる。今回、あるシステムにおいて、利用者が既に一度支払ったデータを誤って再度請求してしまう可能性のあるバグが見つかり、その修正が行われた事例について解説する。
この問題の核心は、「ウォッチモード」という機能を持つ「Actor」と呼ばれるプログラムの動作方法にあった。「Actor」とは、特定のデータソース、例えばウェブサイトやAPIなどから情報を取得する役割を持つ独立したソフトウェア部品のことだ。「ウォッチモード」は、これらのActorがデータソースを定期的に監視し、前回の実行時以降に追加された「新しいデータ」のみを取り込むための機能である。これにより、システムは常に最新の情報を取得しつつ、無駄な処理やデータの重複を防いでいる。
データを重複して取得しないために、Actorは「IDの基準線」という仕組みを利用している。これは、これまでに取得・配信したデータの固有の識別子(ID)を記録しておくリストのようなものだ。新しいデータが来た際、そのIDがこの基準線に含まれていなければ「新しいデータ」と判断し、含まれていれば「既に処理済み」としてスキップする。このIDの基準線は、データの保存場所である「キーバリューストア」に保存される。キーバリューストアとは、データを「キー」と「値」のペアで管理するシンプルなデータベースのことで、今回の場合はデータのIDが「値」として保存される。
このIDの基準線は、際限なく大きくなるのを防ぐために、固定の最大容量が設定されている。これをWATCH_KEEPと呼び、データソースの量に応じて5,000、20,000、あるいは60,000個といった値が設定される。この容量がいっぱいになると、最も古いIDから順に自動的に削除(eviction)される仕組みになっている。ここまでは、ストレージの効率化を考えれば一般的な設計であった。
しかし、問題はこの「IDの削除」の方法にあった。削除される際に、そのIDが将来的に再びデータソースに現れる可能性があるかどうかをシステムが確認せず、無条件かつ静かに削除されてしまっていたのだ。この設計の欠陥が、今回のバグを引き起こす原因となった。
具体的にバグが発生するのは、一度のデータ取得(ポーリング)でWATCH_KEEPの容量を超える大量の新しいIDがデータソースから返された場合だ。この状況では、IDの基準線は新しいIDで満たされ、既に取得済みであるにもかかわらず、まだデータソース上には存在し続けている「古いID」が、基準線から押し出されて削除されてしまう。すると、次のデータ取得の際、システムは以前に取得したはずのこれらの「古いID」を、基準線に存在しないため「新しいデータ」だと誤って判断してしまう。その結果、これらのデータは再度取得・配信され、利用者は既に一度支払ったデータに対して二重に課金されてしまうことになる。
このバグは、実際のActorである「google-play-reviews-scraper」などで具体的に再現された。このActorのWATCH_KEEPは20000に設定されていたが、最初のデータ取得(シードラン)でこの上限をはるかに超える数のレビューIDが返された。その結果、基準線から多くのIDが削除された。その後の増分実行では、本来スキップされるべきであったデータが40行も配信された。この「スキップされたデータが0件」という状態こそが、二重課金が発生していることを示す明確なサインであった。同様の現象は、「court-records-scraper」や「fda-recall-scraper」など、他の複数のActorでも確認され、一度のポーリングで大量のデータが取得される可能性がある全てのソースに共通する問題であった。
このバグがなかなか発見されなかったのには、いくつかの理由がある。まず、Actorの実行自体は完全に正常に「成功」として終了していたため、システム側からはエラーとして検出されなかった。取得されたデータ数も、一見すると妥当な範囲に見えてしまい、異常を知らせるものがなかった。Actorの視点から見れば、基準線にないデータを提供しただけなので、指示通りに動作したことになる。問題は「システム内部の正しい動作」ではなく、「課金」という、別の側面で発生していた。そのため、このバグは実行直後ではなく、将来の請求書で初めて明らかになるという性質を持っていた。また、「実行が途中で停止した」などのステータスメッセージも表示されないため、発見は非常に困難であった。
この問題を解決するために、システムはいくつかの修正を導入した。根本的に二重配信を止めるためには、IDの基準線を無制限にするか、実際のデータ量に合わせて容量を動的に調整するような、より大規模な設計変更が必要になる。しかし、今回の修正では、まずはこの問題が「発生していること」を可視化し、測定できるようにすることに主眼が置かれた。
具体的には、以下の5つの方法で情報が提供されるようになった。
- ログ警告: IDが基準線から削除される際に、システムログに警告メッセージを記録する。
- ステータスメッセージ: Actorの実行結果に付随するステータスメッセージに、IDの削除が発生した旨の情報を追記する。
- 監視記録: 監視レコード自体に、累積でどれくらいのIDが削除されたかを示す情報を記録し、次回の実行時にも参照できるようにする。
- 実行サマリー: 実行のサマリー情報に、削除されたIDの数を追加し、ウェブフックなどを通じて外部の連携システムからも確認できるようにする。
- ドキュメント:
WATCH_KEEPの容量がどのくらいであるかを公式ドキュメントに明記し、利用者が事前にこの制限を知れるようにする。
これらの修正により、二重課金の可能性を検出し、その影響を把握することが可能になった。利用者は、これらの情報をもとに、基準線の容量を増やすなどの対策を講じるかどうかの判断ができるようになる。
この修正が正しく機能するかどうかを確認するため、徹底したテストが行われた。テストでは、ライブのデータソースに対して検証を行った。まず、Actorの一時的なコピーを作成し、WATCH_KEEPの値を意図的に非常に小さい数字に設定する。これにより、通常の実行でも簡単に基準線の上限を超える状況を作り出すことができる。この設定で、最初に大量のデータを取得する実行(シードラン)を行い、次に増分実行をもう一度実行する。この2回の実行で、「取得されたデータが0件スキップされる」という、二重課金が発生していることを示す状況が再現されることを確認した。また、意図的に削除が発生しないように設定したケースでは警告が表示されないことも確認し、システムが適切に動作していることを裏付けた。
この修正は現在、「clinicaltrials-scraper」や「google-play-reviews-scraper」など、ウォッチモード機能を利用する全てのActorに適用されている。この事例は、一見するとシステムが正常に動作しているように見えても、その裏側で利用者にとって不利益となるような問題が発生する可能性があることを示している。そして、その問題を発見し、利用者に適切な情報を提供することで対処する重要性を改めて教えてくれるものだ。