追記型の保存処理は、二重実行で必ず壊れる

2026.09.01 09:00
2026.08.30 00:35
追記型の保存処理は、二重実行で必ず壊れる

AIが作った候補をUIで確認し、「反映」ボタンを押したら本データへ追加する。仕組みとしては単純ですし、最初に動かしたときも問題なく見えました。

ところが運用を始めると、3本のはずが6本、4本のはずが8本と、同じ内容が丸ごと2セット入る事故が続きました。追記という仕様が悪いのではありません。同じものを2回追記できるのに、2回目を見分ける手段がなかったのが問題でした。

消えたマーカーを未処理と誤解する

1. AIが下書きと候補マーカーを書く
2. 人が反映し、マーカーだけが消費される
3. 別のAIがマーカーを見つけられず、未作成と判断する
4. 作り直した候補が再び反映される

マーカーがない状態は未処理ではなく、反映済みかもしれません。消費されるトークンと実データを分けるなら、その意味を実装と文書に明記します。

重複は本体で判定する

ID、連番、日時、スコアは保存ごとに変わるため、比較から除外します。

const key = ({ id, seq, createdAt, score, ...body }) =>
  JSON.stringify(body);
const seen = new Set(existing.map(key));
const isDuplicate = seen.has(key(incoming));

追記処理は二重実行時の挙動をあらかじめ決めます。上書きならべき等にし、追記なら重複検出を必須にします。

スキップした場合は黙らず、「3件中2件は登録済みのため、1件だけ追加した」と通知します。何も出さないと、利用者は反映に失敗したと考えて再実行するからです。

この事故以降、「追加する処理」を見るときは、まず2回実行されたらどうなるかを考えるようになりました。AIが絡むかどうかにかかわらず、消費されるマーカーだけを状態の根拠にしない、本体で重複を判定する、スキップした事実を伝える。この3つがそろうと、追記処理はかなり落ち着きます。

「見つからない」は、未作成とは限りません。反映済みだから消えている可能性まで含めて状態を設計しておくことが、結局いちばん確実な二重実行対策でした。

今回は以上です。