Web Api (REST サービス) における同時実行制御 (ETag と Concurrency Management)


環境 : Visual Studio 2010

REST サービス / Web Api の実践

ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「Getting Started with ASP.NET Web Api」 (ASP.NET の場合)、または「REST サービスの作成」 (WCF の場合) を参照してください。

こんにちは。

今回は、ASP.NEt Web API などの Web Api サービスで、同時実行性をどのように制御すべきか、といった点を再整理してみます。

 

Web の世界における同時実行性の制御

HTTP に忠実な REST の方式では、処理は stateless を前提としているため、Pessimistic ではなく、Optimistic な Concurrency 制御 (すなわち、楽観的同時実行制御) が好まれます。
実際、W3C では、こうした制御のための仕様を既に策定しており、HTML/1.1 では、以前、こちら でも説明した ETag (Entity Tag の略)、If-Match などの HTTP ヘッダーを使ってこれらを制御するよう定めています。(したがって、OData などでも、この仕様を使って同時実行制御を解決します。)
例えば、あるコンテンツを参照した際の ETag ヘッダーの値が "1" で、2 回目に参照した際にも ETag ヘッダーの値が "1" の場合には、そのコンテンツは「変更されていない」ということを意味しています。(ETag はクォートされた文字列であれば良く、 "1"、"2" などのバージョン番号を表す文字列や、"aaaa-aaaa-aaaa-aaaa" 形式のような GUID 形式の文字列でも構いません。)

補足 : ETag には strong と weak があり、Strong ETag は、コンテンツが変更されない前提で、コンテンツのすべての内容 (Header、Body) が同一であることを意味します。一方、Weak ETag は、同じ URI でコンテンツが変更される前提で (例えば、Web 上の参照カウンターなど)、そのコンテンツが、(内容としてまったく一致していなくても) 意味的に同一であることを意図しています。
Weak ETag は、W/"1" のように、W/ のプレフィックスが付与されます。

なお、SharePoint などでは、この Weak ETag が採用されているので、アプリケーション構築などの際には注意してください。

この ETag の値を使用して、下記の HTTP ヘッダーを使用した Request を送信することで、条件付きの抽出や、条件付きの更新処理を要求することができます。もちろん、今回テーマとしている同時実行制御の目的でも使用することができます。 (下記のヘッダーも、HTTP/1.1 の仕様です。)

If-Match このヘッダーで指定した値 (ETag の値) とサーバー上で保持しているデータの値 (ETag の値) が同じであれば、処理 (PUT など) をおこないます。
例えば、HTTP の PUT メソッドでデータの変更を要求する場合、もし、クライアントで保持しているデータが最新 (つまり、他のクライアントから変更されていない) ならば変更を実行し、別のクライアントにより変更されていたら処理しない、といった要求をおこなう場合、このヘッダーが使えます。(つまり、今回テーマとしている Optimistic 同時実行制御の目的で使用できます。)
If-None-Match このヘッダーは、上記とは逆に、指定した値と、サーバー上の最新の値が同じ場合 (ETag が一致する場合) は処理をおこなわず、一致しない場合のみ処理をおこなうよう要求します。
例えば、何かの操作をおこなう場合、もしクライアント側で保持しているコピーが最新ならそれをそのまま使用し、そうでない場合のみ GET をおこないたい場合に、このヘッダーが使用できます。
If-Range このヘッダーは、指定した値と、サーバー上の最新の値が同じ場合 (ETag が一致する場合) は、Range ヘッダーで指定した部分的なデータを取得し、一致しなければデータ全体を取得しなおすといった場合に使用します。(Range ヘッダーと共に使用します。)
例えば、大きなデータを複数回に分割して何かの処理をおこなう場合などに、その途中でデータが変更されてしまった場合には、変更されたデータを再度取得しなおして評価したい場合に、このヘッダーが使えます。

なお、上記で、確認の結果、処理されなかった場合は、その内容に応じたステータス コード (HTTP の StatusCode) を返します。例えば、If-None-Match ヘッダーを使用したリクエストで、ETag が一致していて結果 (Response の Body) を返さなかった場合は、ステータス コード 304 (Not Modified) を返すのが礼儀です。

補足 : 以前、こちら でも説明しましたが、If-Match ヘッダーで「*」(アスタリスク) を指定した場合は、「Any」を意味し、上記のようなチェックをおこなわず、常に Match したものとして処理されます。

補足 : なお、ここでは詳細を述べませんが、Web Api で同時実行を制御する他の手段として、OData の Batch Update を活用する方法もあります。Batch Update は、MIME の Multipart によって複数のアイテムを同時に処理する手法で、OData の仕様に従えば、これは順序の保障や、トランザクションを管理するためものではなく、あくまでも、 throughput を向上させるための機能です。しかし、サービス側の実装によっては、この 1 つの Batch Update を 1 トランザクションとして処理するように実装することもできます。(つまり、複数の更新を 1 つのまとまりとして、short-term での一貫性を保持する目的で使用できます。) どのように処理するかは、サービスの実装に依存します。

 

Web Api における実装

2014/03 追記 : ASP.NET Web API 2.2 では、ETag がサポートされます !

さて、以上は、単に、W3C (HTTP Specification) の仕様の話ですが、Web Api (REST サービス) で、こうした制御を実装するにはどうすれば良いでしょうか ?

基本的には、上記の仕様に沿って、開発者自身が処理する (コードを記述する) 必要があります。つまり、上記で、「・・・のように処理します」、「・・・されます」と記載していますが、こうした仕様に準じたサービスの振る舞いを実装するのは、サービスを構築する開発者の皆さん自身です。(Web サーバーなどが自動化してくれるわけではありません。)

ただし、ASP.NET Web API を使用している場合には、「Web Api (REST サービス) における IoC (関心事の分割)」で解説する方法を使って、ビジネス ロジックとシステム ロジックなどを分離して構築できるので、こうした手法を活用して、ETag の処理を、再利用性の高い形で分離して組み合わせると良いでしょう。(ここでは、このサンプル コードの紹介は省略します。)

 

実装例 (WCF REST のヘルパー関数)

また、WCF を使用している場合には、いくつかのヘルパー メソッドが提供されていますので、必要に応じ、適宜、利用して頂くと良いでしょう。(以下のメソッドは、ASP.NET Web Api WCF Web Api では使用できません。)

補足 : これらのヘルパー メソッドについて、下記でサンプル コードを使って解説しますが、詳細は「MSDN : Conditional Get / Conditional Put」に書かれています。

例えば、PUT メソッドで、下記太字の通り CheckConditionalUpdate メソッドを使用すると、If-Match ヘッダーが "2" の場合、以降の処理が実行されます。もし、If-Match ヘッダーが "2" 以外の場合には、ここで WebFaultException が発生し、ブラウザー側にはステータス コード 412 (Precondition Failed) が返されます。(以降の処理はおこなわれません。)

補足 : このため、デバッグ実行をおこなうと、例外が発生してコードの実行が停止しますので注意してください。ステータス コードを確認するには、デバッグなしでサービスを開始し、Fiddler などのクライアントで確認すると良いでしょう。

. . .

[ServiceContract]
public class Service1
{
  [OperationContract]
  [WebInvoke(Method = "PUT",
    UriTemplate = "orders/{id}",
    RequestFormat = WebMessageFormat.Json,
    ResponseFormat = WebMessageFormat.Json,
    BodyStyle = WebMessageBodyStyle.Bare)]
  public void UpdateOne(OrderItem item, string id)
  {
    WebOperationContext.Current.IncomingRequest.CheckConditionalUpdate("2");

    // execute update ! (skip this code ...)
    . . .
  }
}

public class OrderItem
{
    public int Id;
    public DateTime Time;
}
. . .

実行結果は、下記の通りです。なお、If-Match で * (アスタリスク) を指定しても、ちゃんと合格 (StatusCode 200) します。

[実行結果 1]

PUT http://localhost:34198/Service1.svc/orders/3 HTTP/1.1
User-Agent: Fiddler
Host: localhost:34198
Accept: application/json
If-Match: "2"
Content-Type: application/json
Content-Length: 46

{"Id":3,"Time":"\/Date(1317282271654+0900)\/"}
HTTP/1.1 200 OK
. . .

[実行結果 2]

PUT http://localhost:34198/Service1.svc/orders/3 HTTP/1.1
User-Agent: Fiddler
Host: localhost:34198
Accept: application/json
If-Match: "1"
Content-Type: application/json
Content-Length: 46

{"Id":3,"Time":"\/Date(1317282271654+0900)\/"}
HTTP/1.1 412 Precondition Failed
ETag: "1"
. . .

補足 : 上記のサンプルでは、成功時に 200 を返していますが、現実の開発では、201 など、状況に応じた正しいステータス コードを返すようにしてください。(上記は、サンプルです。)

同様に、GET の際には、CheckConditionalRetrieve メソッドを使用して、If-None-Match ヘッダーに応じた例外を処理できます。
例えば、GET メソッドで下記の通り記述すると、If-None-Match ヘッダーが "2" の場合に WebFaultException が発生し、ブラウザー側にはステータス コード 304 (Not Modified) が返されます。(それ以外の場合は、GET に成功し、Json の Body が返されます。)

. . .

[OperationContract]
[WebGet(UriTemplate = "orders/{id}",
  ResponseFormat = WebMessageFormat.Json)]
public OrderItem GetOne(string id)
{
  WebOperationContext.Current.IncomingRequest.CheckConditionalRetrieve("2");

  WebOperationContext.Current.OutgoingResponse.SetETag("2");
  return new OrderItem
  {
    Id = int.Parse(id),
    Time = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(
      DateTime.UtcNow, "Tokyo Standard Time")
  };
}
. . .

[実行結果 1]

GET http://localhost:34198/Service1.svc/orders/3 HTTP/1.1
Accept: */*
If-None-Match: "1"
Host: localhost:34198
HTTP/1.1 200 OK
Content-Length: 46
ETag: "2"
Cache-Control: private
Content-Type: application/json; charset=utf-8
. . .

{"Id":3,"Time":"\/Date(1317286342655+0900)\/"}

[実行結果 2]

GET http://localhost:34198/Service1.svc/orders/3 HTTP/1.1
Accept: */*
If-None-Match: "2"
Host: localhost:34198
HTTP/1.1 304 Not Modified
ETag: "2"
. . .

なお、サービス構築の際、ETag の情報 (バージョン情報) をどのように管理するか、という点も課題となるでしょう。アプリケーション側で独自に管理しても良いですが、SQL Server を使用している場合は、rowversion 型 (ただし、バイナリー データなので、エンコードなどをおこなってください) や timestamp 型なども使用できますので、使用するアプリケーションの内容に応じて、適切な管理をおこなって頂くと良いでしょう。

 

Web の世界は、契約に基づく世界です。WCF Data Services や SharePoint などを使用した場合は、こうした処理はツールが自動化してくれますが、WCF では「処理の自由度」が高い分、こうした細かなマナーも開発者自身が意識して実装する必要があります。
仕様に忠実な (クリーンで、礼儀正しい) RESTful サービスを、是非、志してください。

 

Comments (0)

Skip to main content