松崎 剛 Blog

This Blog's theme : エンタープライズ開発 (Server side)、Office サーバ開発

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

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

  • Comments 0

環境 :
Visual Studio 2010

REST サービス / Web Api の実践

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

こんにちは。

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

 

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 における実装

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

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

ただし、ASP.NET Web API WCF Web Api を使用している場合には、「REST サービス (Web Api) における 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 や、WCF Web API の Test Client などのクライアントで確認すると良いでしょう。(2012/02 追記 : ASP.NET Web Api では、Test Client は提供されていませんので注意してください。)

. . .

[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");

        // 更新処理を実行 (コードは省略)
        . . .
    }
}

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 サービスを、是非、志してください。

 

Leave a Comment
  • Please add 6 and 6 and type the answer here:
  • Post