理系学生日記

おまえはいつまで学生気分なのか

REST API における同時並行制御

以前「API で同時に更新要求があったとき、どうするのが定石なんだろう」というのを調べたのですが、きちんとまとめていませんでした。 それからちょっと時間がかかってしまいましたが、簡単にここでまとめてみます。

取り組む問題

更新系の操作を行うとき問題になるのは、1 リソースに対して同時並行的な更新が行われようとした場合です。

たとえばですが、

  1. A が口座の貯金額を参照し、$100 あることを確認する
  2. B が口座の貯金額を参照し、$100 あることを確認する
  3. A が $100 に対して $10 を追加し口座を更新する
  4. B が $100 に対して $1 を追加し口座を更新する (結果として、A の更新が失われてしまう)

これでもし 3. で行った A の操作が失われたとしたら、それは大きな問題になります。いわゆる Lost Update Problem ですね。

これらを防ぐためには、ご存じのように、一般に「悲観的排他制御」と「楽観的排他制御」の 2 つの方法があります。 HTTP は曲がりなりにも stateless なプロトコルであることもあり、楽観的排他制御を使うことが多いです。 もちろん、競合頻度が低いと想定されることが前提ではありますが。

勧告

REST API における更新競合については、「経験的に」Etag を使えばええという話になるわけで、事実結論としてはそういう方向になります。 しかし、じゃぁその Etag についてはどこで誰が言っとるんや、というと???というかんじでした。 そこでちょっと調べてみたのですが、Lost Update Problem に対する HTTP での対処法に関する勧告は、W3C と IETF それぞれで述べられています。

前者の勧告では、HTML やテキストファイルといったものを題材に、HTTP 上で Lost Update Problem をどう検知し解決していくかということを記述しています。 ここでは既に後者で定義される EtagIf-match を使って lost update を検知することが説明されています。

概要

Lost Update Problem に取り組む際は、更新者が「他の人はまだ更新していないかな」という確認をどのように行うか、が肝になります。 このためには「ぼくの更新対象のリソースが、ぼくが更新するまで誰にも更新されてないですよね」という確認ができれば良い。

誰にも更新されていないのであれば、「ぼく」が更新したって誰の更新とも衝突しません。従って、そのまま更新を完了させて良いのは自明です。 一方で、誰かが更新してしまっているときにはいくつかの戦略があります。

  1. 誰かの更新と「ぼく」の更新とをマージする
  2. 「ぼく」が「誰か」が更新したリソースを新しく手に入れて、そこから編集をしなおす
  3. 「ぼく」が「誰か」の更新を上書きする (Lost Update)

ここで 3. については単なる上書きですし Lost Update そのものなので除外するとして、多くの場合は 2. を取ることが多いと思います。 W3C でもちょこっと触れられていますが、

  • リソースに対する操作として「マージ」が必ずしも可能とは限らない

ということが大きいのであろうと思います。

実現

というわけで、上述した 2. の流れを実現するために必要なものは、

  • "ぼくの更新対象のリソースが、ぼくが更新するまで誰にも更新されてないですよね"の確認

になります。 もうすこし明確に言い換えると、

  1. 更新者が「編集」を行う際、そのオリジナルとなるリソースをサーバから入手する必要がある。このタイミングでサーバが「リソース」の状態が分かる一意識別子を返却する。
  2. 更新者がサーバに更新を行わせるとき、「サーバ上のリソースがこの一意識別子に対応する値である(つまりは更新されていない)ときにだけ更新してくれ」と頼む。

ということになります。

一意識別子に該当するのが Etag であり、2. の「〜のときにだけ更新してくれ」が Precondition と呼ばれる概念です。

Etag

Etag というのは、対象リソースの状態を文字列で表現したものです。文字列自体に意味はなく(opaque)、状態を一意に表現できれば良いというものです。

RFC 7232 を確認すれば分かるように Etag には 2 種類が存在しますが、排他制御の文脈で使われるのは Strong Etag の方でしょう。

  1. Weak Etag
  2. Strong Etag

RFC には、以下のように語られています(validator というのは etag を含む概念です)。"lost update" の語句が出てますね。

Strong validators are usable for all conditional requests, including cache validation, partial content ranges, and "lost update" avoidance.

Weak Etag というのは、必ずしも「対象」となるオブジェクトの変化を反映するとは限りません。RFC にも記載がありますが、例えば

  • 本来は秒単位で変わり得る「天気予報」のようなものを、一定期間同じ Etag の値とする
  • Modification Time のような秒の精度しかないものを Etag の値

とするといったときに使うのが Weak Etag です。これが、排他制御という用途には適さないのは自明だと思います。

逆に、Strong Etag は、対象のオブジェクトが変化したら必ず変化すべき Etag です。

この値の生成は、RFC には明記されていません。なぜなら、何を以ってリソースの状態を一意に表現できるかというのは、サービスの作者が一番良くわかるとされているからです。 具体的な方法としては、

  • 内部で使用しているバージョン番号を使う
  • リソースのコンテンツやファイルの属性に対してハッシュをかける

なんてことが紹介されています。

Precondition

Precondition が表現するのは「条件付きリクエスト」で、「この条件が満たされたときにだけ、要求された処理を実行する」ということになります。 これらはやはり RFC 7232 で定義されていて、

  • If-Match
  • If-None-Match

といったヘッダで表現されます。 ここでの Etag 値は、前にサーバからリソースを入手するときに Etag レスポンスヘッダとして渡された、当該タイミングにおける当該リソースの表現です。

例えば更新要求を API で行うときに If-Match: [Etag 値] というヘッダを送信すると、サーバはそのときのリソースの状態が当該 Etag 値に該当するときにだけ更新を行います。 該当しない場合は、HTTP ステータスコード 412 (Precondition Failed) を返却しエラー応答します。これがまさに、更新競合のケースです。

補足

別に Etag という HTTP ヘッダに固執することはなくて、HTTP BODY 経由で同等のことを実現することもあります。 ちょっと古い情報になってしまいますが、Google Data API なんかも、Etag/If-Match ヘッダだけでなく、 HTTP BODY でそれらを渡す方法を取っていたりします。

参考