前回の最後に出てきた、「残り1個を二人が同時に取ろうとしたら?」という話。
A「じゃあ、ちゃんと聞こうか。」
私「何を?」
A「同時に二人が操作したらどうするの?」
私「そこか。」
A「在庫1個。」
私「うん。」
A「二人とも画面では『残り1』って見えてる。」
私「うん。」
A「ほぼ同時に取ったら?」
私「それはちゃんと確定するときに――」
B「これは典型的な競合状態、Race Conditionですね。複数の処理が同じ古い状態を見たまま、同時に成立しようとする。」
A「そう。それ、画面に『残り1』って出せたから終わりじゃない。」
B「技術的には、そこからトランザクション、排他制御、楽観的ロックなど、確定時の整合性をどう守るかという話につながります。」
私「当時はそんな言葉知らないよ。『最後の1個を二人が取れたらおかしい』だった。」
A「そこは後で詳しく聞く。絶対長くなるから。」
私「一回ちゃんと作れば後は――」
A「……またそれ言ってるな(笑)」
私「何が?」
A「『一回作れば後は楽』。」
私「だって楽になるじゃん。」
A「二回目だからな。まだ言わせておく(笑)」
当時の私は、技術用語から設計していたわけではなかった。先にあるのはいつも、「それだと現場で変じゃない?」だった。その違和感を潰していくと、後から技術の名前が付いてきた。そして在庫についても、まだ全部が実装済みというわけではない。将来的には、消費期限や使用期限が近い在庫を優先して候補に出すことも考えている。
B「期限情報を候補評価に使うなら、優先順位付け、ランキングの考え方ですね。」
A「それももう作ったの?」
私「そこはまだ将来構想。」
A「よかった。」
私「何が?」
A「一個くらい未来に残しておけ(笑)」
でも、同時操作の問題は、未来の話ではありませんでした。予約でも同じことが起きます。
A「じゃあ同じ車の同じ時間を、二人が見てる。」
私「うん。」
A「二人とも画面では『空いてる』。」
私「うん。」
A「ほぼ同時に予約ボタンを押す。」
私「うん。」
A「どうする?」
B「在庫と同じく、競合状態、Race Conditionですね。」
A「やっぱり来た。」
B「画面を開いた時点では二人とも空いていても、一人目の予約が成立した瞬間、二人目にとってはもう空きではありません。」
A「じゃあ画面で重複チェックしただけじゃダメ。」
私「確定するときに、もう一回確認する。」
B「サーバー側で現在の成立条件を再確認する必要がありますね。」
A「そこ、やった?」
私「やった。」
A「……即答した。」
私「そこはちゃんとやったよ。」
A「じゃあ二人が本当にほぼ同時だったら?」
私「それも――」
A「予約ボタン連打されたら?」
私「それも――」
A「通信遅くて、同じ処理がもう一回来たら?」
私「ちょっと待て(笑)」
A「ほら。『重複予約を防ぎました』だけじゃ終われないでしょ。」
B「ここからは単なる重複判定ではなく、競合制御や状態遷移そのものを守る必要がありますね。」
そして予約機能では、さらにキャンセル待ちも実装しました。キャンセル待ち登録から順番到来、確定・辞退、次候補への繰り上げまで実装しています。キャンセル待ち人数の上限や、順番が来た後の確定期限も、予約カテゴリごとに設定できるようにしました。
A「再度確認するが、最初は在庫を見たかっただけだよね?なんで今、お前はWrite Lockと状態遷移の破綻と戦ってんだよ!!(笑)」
私「本当に最初は『何が空いてるか在庫が見たいだけ』だったんだよ!!」
私「気づいたら周りは敵だらけ。」
A「お前がAIと敵のアジトに丸腰で突っ込んで、そこで勝手にシェルター建設してるんだよ(笑)」
私「AIに泣きついて、とりあえず土のう積んでもらっただけなんだけど……」
A「DIYっぽく言うんじゃねえよ(笑)」
B「そのシェルター、単一プロセス内のWrite Lockに、状態機械(State Machine)、確定直前の再チェック、Fail-Closedまで組み合わせて、整合性を守る設計になっています。」
A「じゃあ、どうやってキャンセル待ちの順番と確定競合を守ってるんだ?」
B「単一プロセス前提ですが、更新経路を一本に寄せて、Write Lock → 状態再確認 → 更新 → 保存の順序を固定しています。さらに候補順も決定的な並び順(deterministic ordering)で揃えています。」
A「……土のうじゃねえじゃん(笑)」
例えば、二人が同じ在庫数を見ている。一人目が先に数量を変更した。二人目は、その変更前の古い画面のまま操作する。
私「そのまま保存したら、一人目の変更を潰す可能性あるじゃん。」
B「そこで楽観的ロックです。」
自分が見た時点から、そのデータが更新されていないかを確認する。
A「更新されてなかったら?」
私「そのまま進める。」
A「誰かが先に更新してたら?」
私「そのまま上書きしない。」
B「競合として返す。HTTPで表すなら、409 Conflictですね。」
私「今は409って言われたら分かる。」
A「成長した(笑)」
私「当時は『誰か先に触ってるから、一回見直して』くらい。」
A「利用者から見れば、それでいい。」
そして、同時操作とは少し違うけれど、もう一つあります。二重送信。
A「予約ボタン押しました。」
私「うん。」
A「反応が遅い。」
私「うん。」
A「もう一回押しました。」
私「やりそう。」
A「二回ともサーバーに届きました。」
私「……。」
A「二回予約される。」
私「それは困る。」
B「ここでは冪等性、Idempotencyという考え方が重要になります。」
A「同じ要求が複数回来ても、意図しない二重処理を成立させない。」
B「はい。」
私「当時の俺なら『ボタン二回押しただけで二個減ったらダメじゃん』。」
A「また技術用語にすると急に格好いい(笑)」
さらに、「在庫があるか確認する」「予約できると判断する」「実際に数量を変える」という処理が分かれていたら、その途中に別操作が入る可能性もある。
B「在庫1個を確認した直後、数量を減らす前に別処理がその1個を使う、といったケースですね。」
A「確認した意味なくなる。」
私「だから一連の処理を途中まで成立させたくない。」
B「考え方としては、トランザクションにつながりますね。」
A「確認して、更新して、関連データも変えて、全部成功したら確定。途中で失敗したら戻す。」
B「ロールバックですね。」
A「これはさすがに知ってる(笑)」
私「誰も疑ってないよ(笑)」
A「さっきから『今知った?』みたいに見るから(笑)」
A「じゃあ『同時に何かされたらどうする』編も整理しよう。」
私「どうぞ。」
複数人が同じ古い状態を見て操作する。
B:「競合状態・Race Condition。」
自分が見た時点から更新されていないか確認する。
B:「楽観的ロック。」
他者が先に更新していたら、そのまま上書きしない。
B:「409 Conflict。」
連打・通信再送で同じ処理を二重成立させない。
B:「冪等性。」
関連する複数処理を途中まで成立させない。
B:「トランザクションという考え方。」
※ただし、ここで説明したのはトランザクションという考え方です。現行版はJSONベースなので、一般的なRDBMSのような完全なトランザクションを、すべての更新処理で実現しているわけではありません。一部の処理では、原子的ファイル保存、スナップショットを使った補償ロールバック、競合検知などを使って、途中状態を残しにくくしています。
A「……これ全部、利用者から見たらボタン一個なんだよね?」
私「そうだね。」
A「エンジニアが『そのボタン、裏では結構大変なんですよ』って言いたくなる理由が詰まってる(笑)」
B「見えない部分ほど、データ整合性へ直結しますからね。」
この頃から、私自身も少しずつ、「画面で動いた=完成」ではないと思うようになっていった。画面に表示できた。ボタンが押せた。保存できた。それでも、同時操作されたら? 古い画面だったら? 連打されたら? APIを直接呼ばれたら? 権限が変わっていたら? 会社設定がおかしかったら?見るところが、少しずつ増えていきました。そして次に作ったのが、
マイ在庫。
A「……まだ在庫増やすの?」私「増やす。」
次回会社の在庫がある。現場にも在庫がある。でも、現場仕事をしていると、自分で持っている材料もあります。車に積んでいる物。自分の棚に置いている物。だったら、それも自分専用の在庫として持てばいい。
最初は、それだけのつもりでした。――「どこにあるか」と「誰の物か」が、別の問題だと気づくまでは。
連載公開分(クリックで開く)
- # 【プロローグ】素人エンジニアが自分でアプリ開発できると甘く見てた 全16弾無邪気な実装 vs 架空エンジニア2人の容赦なきツッコミ ※便利かなと思ってどんどん追加しちゃうんだよね
- #1「在庫が見たいだけ」だったのに、引き算ひとつから在庫引当と多目的最適化まで始まった ※総在庫・予約在庫・有効在庫を分けた瞬間、ただの在庫表示が別物になった
- #2「予約機能もこのアプリ1個にまとめれば便利じゃん」で、“時間”が一気に牙をむいた ※時間重複・境界条件・時間粒度・スマホ操作まで、空いてる時間を選ぶだけでは終わらなかった
- #3 「機能増えたら毎回探すの面倒じゃん」で、お気に入りから共通UI設計まで広がった ※機能を増やした結果、「何を作るか」より「どう辿り着くか」が問題になった
- #4「普通の組織図でいいじゃん」で一人を複数部署に入れたら、“多対多”が始まった ※兼務・複数所属・部署別役職・過去履歴が一気に絡み始めた
- #5「どこの現場で使うかも持てばいいじゃん」で、物件マスタが在庫・予約・人を全部つないだ ※機能ごとにバラバラだった情報が、共通マスタを境に一つのシステムへ変わり始めた
- #6「会社ごとにちょっと変えたい」で、設定駆動とバリデーションが殴り合いを始めた ※柔軟にしたら、「自由に設定させすぎない仕組み」の方が難しくなった
- #7「ログインできたからって何でも触れたらまずいじゃん」で、認証より面倒な認可の沼に入った ※管理者か一般かでは足りず、「誰が・誰に・何をできるか」まで判定することになった
- #8「二人とも空いてる画面を見てたら、どっちが勝つの?」でRace Conditionが牙をむいた ※在庫も予約も、画面に表示された「空き」を信用しただけでは守れなかった
- #9「自分の在庫も持てた方が楽じゃん」で、“場所・所有・利用範囲”が全部別問題だった ※マイ在庫・現場在庫・共有在庫を分けたら、在庫数よりスコープ管理が難しくなった(近日公開予定)
- #10「申請もこの中でできた方が楽じゃん」で、CRUDの裏に状態遷移が増殖した ※申請・承認・差し戻しを入れた瞬間、「保存できる」だけでは何も足りなくなった(近日公開予定)
- #11「資格もQRで見られたら早いじゃん」で、“全部見せる”より“必要な答えだけ返す”方が難しかった ※資格・個人QR・権限をつないだら、識別より情報開示の方が本題になった(近日公開予定)
- #12「一輪車を“ネコ”で探せないの不便じゃん」で、Canonical IDと人間の曖昧さを両立させることになった ※人間には優しく、内部は厳密に−−別名検索から正式IDへ収束させるまで(近日公開予定)
- #13「機能増えすぎて説明する方が面倒」で、“どれが今の正解?”問題がコードの外まで広がった ※ヘルプ・仕様・会話・引き継ぎがズレ始め、Single Source of Truthが必要になった(近日公開予定)
- #14「Codexなら全部見てくれるじゃん」で、AIの作業速度が人間のレビュー限界を超えた ※コードベース横断調査が便利すぎて、変更範囲・Git・差分・安全コミットが主役になった(近日公開予定)
- #15「テストこんなに通るじゃん」で安心しかけたら、“全部緑なのに間違ってる”が普通に出てきた ※横断テストで見えた、Interaction Bug・境界値・期待結果そのものの罠(近日公開予定)
- #16「人間+AI+Codex、ここまで来たら完成でしょ?」で、開発体制そのものをプロにダメ出しされた ※完全な、ど素人から辿り着いた現在地−−でも最後まで人間が働きすぎだった(近日公開予定)
- ♯エピローグ「作れた」で終わりじゃなかった。むしろ、そこからが本番だった ※やっぱりプロのエンジニアは凄いですね。(近日公開予定)
