January 2010

Volume 25 Number 01

クラウド パターン - Windows Azure 向けサービスの設計

Thomas Erl、, Arman Kurtagic、, Herbjörn Wilhelmsen | January 2010

コード サンプルのダウンロード

Windows Azure は、マイクロソフトが開発中の新しいクラウド コンピューティング プラットフォームです (microsoft.com/windowsazure、英語)。開発者はクラウド コンピューティングを利用して、インターネットにアクセス可能な仮想環境でアプリケーションをホストできます。仮想環境では、ユーザーが気にかけることなく、アプリケーションに必要なハードウェア、ソフトウェア、ネットワーク、およびストレージが提供されます。

他のクラウド環境と同様、Windows Azure もアプリケーションのホスト環境を提供します。Windows Azure では、これまでの Windows デスクトップとの違いを最小限に抑えながら、.NET Framework アプリケーションを配置できる新たなメリットが追加されます。

サービスやアプリケーションをクラウド コンピューティングの新たな領域に移行する際に成功を収める鍵となるのは、サービス指向アーキテクチャ (SOA) のパターンを適用することと、サービス指向のソリューションを実装するときに集めた経験を活かすことです。そこで、Windows Azure でサービスを配置するときに、SOA のパターンをどのように適用するかを理解するために、架空の銀行がサービスをクラウドに移行するシナリオについて見てみることにしましょう。

クラウド バンキング

Woodgrove Bank は小さな金融機関で、Woodgrove Bank Online というブランドの新しいオンライン バンキング構想を展開していくことに決めています。Woodgrove Bank の最重要クライアントの 1 社である Fourth Coffee は、この構想を支援するために、カード取引を処理する新しいソリューションを試してみることにしました。このソリューション用に計画したサービスのサブセットは既に実用化されており、それらのサービスの可用性の高さから他の顧客の関心も高まっています。しかし、ソリューションの展開計画が増加するにつれて、いくつか課題が浮かび上がってきました。

まず浮かび上がったのは、スケーラビリティと信頼性に関する問題です。Woodgrove Bank は、IT ソリューションのホスティングを担当することは考えていませんでした。そのため、Sesame Hosting Company という地元の ISP とプロビジョニング契約を締結しました。これまで、Sesame Hosting は Woodgrove Bank の Web ホスティングに関するニーズを満たしてきましたが、新しいカード処理ソリューションでは、同社の対応能力を上回るスケーラビリティ要件が求められています。

Woodgrove Bank のテクノロジ アーキテクチャ チームは、Redundant Implementation (冗長実装) パターンに従って、Woodgrove Bank Online サービスを冗長配置することを提案しています (ここで説明しているパターンについては、soapatterns.org を参照してください)。要するに、スケーラビリティを高め、フェールオーバーを確実に行うために、意図的にサービスを余分に配置する手法を提案しています。Sesame Hosting はこの提案を調べましたが、サービスを余分に配置するために自社のインフラストラクチャを拡張する余裕がありません。端的に言えば、必要なハードウェア、運用ソフトウェアのメンテナンス、およびネットワーク機器の増加に対処するリソースや予算がありません。

スケジュールも問題です。同社が必要なインフラストラクチャを準備したとしても、Woodgrove Bank が計画した展開スケジュールに間に合わせることができません。自社でスタッフを採用してトレーニングする必要があるため、インフラストラクチャの拡張には Woodgrove Bank の予定をはるかに超える時間がかかります。

Woodgrove Bank のチームは、Sesame Hosting ではニーズを満たせないとわかったため、パブリック クラウドでサービスをホストすることを模索し始めました。Windows Azure プラットフォームでは、Redundant Implementation パターンを問題なく適用できるサービスの仮想化方法が提供されます。この Windows Azure の機能は、オンデマンドのアプリケーション インスタンス (2009 年 5 月号で説明しました) と呼ばれます。この長期コミットメントを必要としない、マイクロソフトのデータセンターを使用する機能は、Woodgrove Bank のチームにとっては期待が持てると思われます。では、Woodgrove Bank が Windows Azure にソリューションを移行する方法を詳しく見ていくことにしましょう。

配置の基礎

最優先の作業は、標準化されたサービス コントラクトの原則に準拠するコントラクト ファースト手法に従って、Web サービスを配置することです。Woodgrove Bank のチームは、WSCF.blue ツールを使用して、WSDL と XSD から、最適に相互運用を行う目的でモデル化された Windows Communication Foundation (WCF) コントラクトを生成します。図 1 に生成されたサービス コントラクトを示します。

図 1 最初のサービス コントラクト

最初のサービス コントラクト

サービスは時間の経過とともに変化し、進化していく必要があるため、データ コントラクトに IExtensibleObject インターフェイスを実装して、Forward Compatibility (上位互換性) パターンもサポートすることにします (図 2 参照)。

図 2 最初のデータ コントラクト

最初のデータ コントラクト

Woodgrove Bank のチームは、必要なデータを格納するために SQL Azure を使用することを考えています。というのも、SQL Azure には、チームが保持したいと考えている既存のデータベース構造が既に用意されているからです。リレーショナル ストア以外を使用できるのであれば、Windows Azure Storage の使用も検討できます。

Woodgrove Bank のアーキテクトは、Visual Studio Template Cloud Service の作成に進み、Visual Studio を使用して公開します。その後、Windows Azure ポータルにログインして、新しいクラウド サービスを作成します (図 3 参照)。

図 3 Windows Azure ポータルでのサービスの作成

Windows Azure ポータルでのサービスの作成

次に、サービスの配置を開始できる画面が表示されます。[Deploy] (配置) をクリックし、アプリケーション パッケージ、構成設定、および配置名を指定します。さらに数回クリック操作を行えば、サービスがクラウドに配置されます。

サービス構成の例を図 4 に示します。

図 4 Windows Azure のサービス構成

<Role name="BankWebRole">
  <Instances count="1" />
  <ConfigurationSettings>
    <Setting 
      name="DataConnectionString" 
      value="DefaultEndpointsProtocol=https;AccountName=YOURACCOUNTNAME;AccountKey=YOURKEY" />
    <Setting 
      name="DiagnosticsConnectionString" 
      value="DefaultEndpointsProtocol=https;AccountName=YOURDIAGNOSTICSACCOUNTNAME;AccountKey=YOURKEY" />

Woodgrove Bank のスケーラビリティ要件に関して、ソリューションの柔軟性を高めるために重要なのは次の構成要素です。

<Instances count="1" />

たとえば、10 個のインスタンスが必要であれば、この要素を次のように設定します。

<Instances count="10" />

図 5 は、インスタンスが 1 つだけ実行されていることを確認する画面を示しています。[Configure] (構成) をクリックすると、必要に応じてサービス構成を編集し、インスタンスの設定を変更できる画面が表示されます。

図 5 Windows Azure で実行中のインスタンス

Windows Azure で実行中のインスタンス

パフォーマンスと柔軟性

Woodgrove Bank の開発チームは、負荷テストの実施後、SQL Azure に集中管理用のデータ ストアが 1 つしかないと、トラフィックが増加したときに応答時間が徐々に遅くなることに気付きました。開発者は Windows Azure テーブル ストレージを使用して、このパフォーマンスの問題に対処することにしました。Windows Azure テーブル ストレージは、多数のストレージ ノード間にパーティションを分散することによって、スケーラビリティを向上するように設計されています。また、Windows Azure テーブル ストレージを使用すると、システムによってパーティションの使用状況が監視され、パーティションの負荷が自動的に分散されるため、すばやくデータにアクセスできます。ただし、Windows Azure テーブル ストレージはリレーショナル データ ストアではないので、チームはいくつか新しいデータ ストレージ構造を設計し、応答時間を短くするパーティションと行のキーの組み合わせを選択する必要がありました。

最終的には、図 6 に示す 3 つのテーブルになりました。UserAccountBalance テーブルには、ユーザーの口座残高を格納します。AccountTransactionLogg テーブルは、特定の口座に対するすべての取引メッセージを格納します。UserAccountTransaction テーブルには口座の取引を格納します。UserId と AccountId はすべてのクエリに含まれており、応答時間を短縮できるので、UserAccountTransaction テーブルと AccountTransactionLogg テーブルのパーティション キーはこの 2 つを連結して作成しました。UserAccountBalance テーブルのパーティション キーは UserId で、行キーは AccountId です。UserId と AccountId を組み合わせて、ユーザーとそのユーザーの口座の一意識別子を提供します。

図 6 Windows Azure テーブル ストレージ モデル

Windows Azure テーブル ストレージ モデル

Woodgrove Bank は、ここまではプロジェクトが成功していると考え、多くの顧客にこのソリューションの使用を開始してほしいと考えています。World Wide Importers では、間もなくこのソリューションの使用準備が整います。ただし、いくつか新しい機能要件があります。

最も重大だと思われる要件は、サービス インターフェイス (情報構造) を変更する必要があることです。World Wide Importers によると、Woodgrove Bank が使用している情報構造は、World Wide Importers の情報構造と互換性がありません。World Wide Importers は重要な顧客なので、Woodgrove Bank の開発チームは、Data Model Transformation (データ モデル変換) パターンを適用することを提案します。
開発者は、World Wide Importers が要求したインターフェイスを使用していくつか新しいサービスを作成し、それらのサービスに、World Wide Importers のデータ モデルと Woodgrove Bank のデータ モデル間で要求を変換するロジックを含めます。

この要件を満たすために、UserAccount について新しい構造を作成します。図 7 に示すように、UserAccountWwi クラスと UserAccount クラス間に明確なマッピングを注意深く指定します。

図 7 Data Model Transformation の UserAccount 構造

Data Model Transformation の UserAccount 構造

サービス コントラクトは、明確なデータ コントラクト (UserAccountWwi) を受け入れる必要があります。UserAccountWwi は、要求を UserAccount に変換してから、ソリューションの他の部分への呼び出しに渡して、応答時に元の状態に戻すように変換します。Woodgrove Bank のアーキテクトは、こうした新しい要件を実装するときに、基本サービス インターフェイスを再利用できることがわかりました。最終的な設計を図 8 に示します。

図 8 World Wide Importers 向けのサービス コントラクト

World Wide Importers 向けのサービス コントラクト

開発者は、UserAccount クラス用に 2 つの拡張メソッド (TransformToUserAccountWwi メソッドと TransformToUserAccount メソッドなど) を作成して、データ変換を実装することにしました。

新しいサービスは、UserAccountWwi データ コントラクトを受け入れます。他の層に要求を送信する前に、TransformToUserAccount という拡張メソッドを呼び出して、データを UserAccount に変換します。コンシューマーに応答を返す前に、TransformToUserAccountWwi を呼び出して、UserAccount コントラクトを UserAccountWwi に変換して戻します。これらの要素の詳細については、この記事のダウンロード コードに含まれている UserAccountServiceAdvanced のソース コードを参照してください。

メッセージングとキュー

Woodgrove Bank はソリューションを稼働させ、膨大な数の受信要求を円滑に処理できるようになりましたが、アナリストはサービスの使用状況の中で著しいピーク状態が発生していることに気付きました。こうしたピークの中には、定期的 (具体的には、月曜日の午前と木曜日の午後) に発生するものがあります。しかし、変化を予測できないものもあります。

Windows Azure 構成を通じてオンラインにするリソースを増やすことも簡単な解決策の 1 つですが、World Wide Importers などの一部の大手顧客が新しいサービスに関心を持つようになったので、同時使用に起因するこうした変化は増大すると予想されます。

Woodgrove Bank の開発者は、Windows Azure が提供する機能を詳しく調べて、Reliable Messaging (信頼できるメッセージング) パターンと Asynchronous Queuing (非同期キュー) パターンを適用できる機能を見つけました。Reliable Messaging パターンは顧客が選択できる技術が制限されるため、最終的には最適なパターンではないという結論に達しました。Asynchronous Queuing パターンは、顧客が特殊なテクノロジを必要としないで利用できるため、このパターンに的を絞ることにしました。ただし、Reliable Messaging パターンで使用されるテクノロジはすべてマイクロソフトが提供するテクノロジなので、Windows Azure クラウド内ではこのパターンを使用することが理にかなっています。

エラー状態や定期メンテナンスによってサービスがオフラインになるときでも、メッセージが失われないようにすることが目標です。Asynchronous Queuing パターンに適していないサービスもありますが、この目標を実現できるのはこのパターンです。たとえば、オンラインでカード取引を処理する場合、送金の確定や拒否に即答が必要になるため適していませんが、それ以外は Asynchronous Queuing パターンが適しています。

Web ロールと Worker ロール (これらのロールの詳細については、msdn.microsoft.com/ja-jp/magazine/dd727504 を参照してください) 間の通信は、Windows Azure キューを使用して実行されます (CTP 11 月版の時点では、ロール インスタンス間で直接通信することが可能です)。既定では、この通信は非同期かつ信頼性のある方法で行われます。だからと言って、エンド ユーザーと Woodgrove Bank のサービス間の通信に信頼性があると言っているのではありません。実際に、クライアントと、Web ロールに存在するサービスと間の通信方法には明らかに信頼性がありません。Woodgrove Bank のチームは、この問題には対処しないことにしました。というのも、顧客にまで信頼性メカニズムを実装すると、実際には顧客も Woodgrove Bank が選択したのと同じ技術に準拠する必要があるためです。これは非現実的で、好ましくないと考えました。

キューの使用

顧客が UserAccountService にメッセージを送信するとすぐに、このメッセージは Windows Azure キューに格納され、顧客は確認メッセージを受信します。その後、UserAccountWorker はキューからメッセージを取得できます。UserAccountWorker がダウンした場合でも、メッセージはキューに安全に格納されているので失われません。

UserAccountWorker 内での処理が失敗すると、そのメッセージはキューから削除されません。そのため、キューの DeleteMessage メソッドへの呼び出しは、作業が完了したときのみ行われます。UserAccountWorker がタイムアウト時間 (タイムアウト時間は 20 秒にハードコーディングされています) が経過するまでにメッセージの処理を終えなかった場合、UserAccountWorker の別のインスタンスがメッセージを処理できるように、そのメッセージが再びキュー内でアクティブになります。

顧客が UserAccountService にメッセージを送信するとすぐに、このメッセージはキューに格納され、顧客は TransactionResponse 型の確認メッセージを受信します。顧客から見ると、Asynchronous Queuing パターンが使用されます。ReliableMessaging は、UserAccountStorageAction と AccountStorageWorker 間の通信に使用されます。UserAccountStorageAction と AccountStorageWorker は、それぞれ Web ロールと Worker ロールに存在します。呼び出しハンドラーによってキューにメッセージが格納される方法を以下に示します。

public TransactionResponse ReliableInsertMoney(
  AccountTransactionRequest accountTransactionrequest) {

//last parameter (true) means that we want to serialize
//message to the queue as XML (serializeAsXml=true)
  return UserAccountHandler.ReliableInsertMoney(
    accounttransactionRequest.UserId, 
    accounttransactionRequest.AccountId, 
    accounttransactionRequest.Amount, true);
}

UserAccountHandler は、ランタイムに挿入される IUserAccountAction を返すプロパティです。これにより、実装とコントラクトが切り離され、後の実装の変更が容易になります。

public IUserAccountAction<Models.UserAccount> UserAccountHandler
  {get;set;}

public UserAccountService(
  IUserAccountAction<Models.UserAccount> action) {

  UserAccountHandler = action;
}

メッセージは、そのメッセージの処理を担当する操作に送信された後、キューに格納されます。図 9 の最初のメソッドは、シリアル化可能な XML としてデータを格納する方法を示しており、2 つ目のメソッドは文字列としてデータをキューに格納する方法を示しています。Windows Azure キューのメッセージには、最大サイズが 8 KB という制限があることに注意してください。

図 9 データの格納

public TransactionResponse ReliableHandleMoneyInQueueAsXml( 
  UserAccountTransaction accountTransaction){ 

  using (MemoryStream m = new MemoryStream()){ 
    XmlSerializer xs = 
      new XmlSerializer(typeof(UserAccountTransaction)); 
    xs.Serialize(m, accountTransaction); 

    try 
    { 
      QueueManager.AccountTransactionsQueue.AddMessage( 
        new CloudQueueMessage(m.ToArray())); 
      response.StatusForTransaction = TransactionStatus.Succeded; 
    } 
    catch(StorageClientException) 
    { 
      response.StatusForTransaction = TransactionStatus.Failed; 
      response.Message = 
        String.Format("Unable to insert message in the account transaction queue userId|AccountId={0}, messageId={1}", 
        accountTransaction.PartitionKey, accountTransaction.RowKey); 
    } 
  } 
  return response; 
} 

public TransactionResponse ReliableHandleMoneyInQueue( 
  UserAccountTransaction accountTransaction){ 

  TransactionResponse response = this.CheckIfTransactionExists( 
    accountTransaction.PartitionKey, accountTransaction.RowKey); 
       
  if (response.StatusForTransaction == TransactionStatus.Proceed) 
  { 
    //userid|accountid is partkey 
    //userid|accountid|transactionid|amount 
    string msg = string.Format("{0}|{1}|{2}", 
      accountTransaction.PartitionKey, 
      accountTransaction.RowKey, 
      accountTransaction.Amount); 

    try 
    { 
      QueueManager.AccountTransactionsQueue.AddMessage( 
        new CloudQueueMessage(msg)); 
      response.StatusForTransaction = TransactionStatus.Succeded; 
    } 
    catch(StorageClientException) 
    { 
      response.StatusForTransaction = TransactionStatus.Failed; 
      response.Message = 
        String.Format("Unable to insert message in the account transaction queue userId|AccountId={0}, messageId={1}", 
        accountTransaction.PartitionKey, accountTransaction.RowKey); 
    } 
  } 
  return response; 
}

QueueManager クラスでは、構成の定義を使用してキューを初期化します。

CloudQueueClient queueClient = 
  CloudStorageAccount.FromConfigurationSetting(
    "DataConnectionString").CreateCloudQueueClient();

accountTransQueue = queueClient.GetQueueReference(
  Helpers.Queues.AccountTransactionsQueue);
accountTransQueue.CreateIfNotExist();

loggQueue = queueClient.GetQueueReference(
  Helpers.Queues.AccountTransactionLoggQueue);
loggQueue.CreateIfNotExist();

AccountStorageWorker は、AccountTransactionQueue でメッセージをリッスンし、キューからメッセージを取得します。メッセージをリッスンできるようにするには、次のように、Worker が正しいキューを開く必要があります。

var storageAccount = CloudStorageAccount.FromConfigurationSetting(
  "DataConnectionString");
// initialize queue storage 
CloudQueueClient queueStorage = storageAccount.CreateCloudQueueClient();
accountTransactionQueue = queueStorage.GetQueueReference(
  Helpers.Queues.AccountTransactionsQueue);

キューが開かれ、AccountStorageWorker がメッセージを読み取った後、メッセージは 20 秒間キュー内で非アクティブになります (このタイムアウト時間を 20 秒に設定しました)。その間に、Worker はメッセージの処理を試みます。

メッセージの処理が成功すると、メッセージはキューから削除されます。処理が失敗すると、メッセージはキューに戻されます。

メッセージの処理

ProcessMessage メソッドでは、まず、メッセージの内容を取得する必要があります。この処理は、次の 2 つのうち、いずれかの方法で実行できます。1 つは、次のようにメッセージを文字列としてキューに格納する方法です。

//userid|accountid|transactionid|amount
var str = msg.AsString.Split('|');...

2 つ目は、次のようにメッセージをシリアル化した XML にする方法です。

using (MemoryStream m = 
  new MemoryStream(msg.AsBytes)) {

  if (m != null) {
    XmlSerializer xs = new XmlSerializer(
      typeof(Core.TableStorage.UserAccountTransaction));
    var t = xs.Deserialize(m) as 
      Core.TableStorage.UserAccountTransaction;

    if (t != null) { ....... }
  }
}

なんらかの理由で AccountStorageWorker がダウンするか、メッセージを処理できない場合でも、そのメッセージはキューに保存されているため失われません。AccountStorageWorker 内の処理が失敗した場合でも、メッセージはキューから削除されず、20 秒後にキュー内でアクティブに戻ります。

この動作を保証するために、キューの DeleteMessage メソッドへの呼び出しは、作業が完了したときのみ行います。AccountStorageWorker によるメッセージの処理がタイムアウトするまでに終了しない場合、AccountStorageWorker の別のインスタンスがメッセージを処理できるように、メッセージがキュー内で再びアクティブになります。図 10 は、文字列として格納されたメッセージでの作業です。

図 10 キューに格納されたメッセージの処理

if (str.Length == 4){
  //userid|accountid|transactionid|amount
  UserAccountSqlAzureAction ds = new UserAccountSqlAzureAction(
    new Core.DataAccess.UserAccountDB("ConnStr"));
  try
  {
    Trace.WriteLine(String.Format("About to insert data to DB:{0}", str),      
      "Information");
    ds.UpdateUserAccountBalance(new Guid(str[0]), new Guid(str[1]), 
      double.Parse(str[3]));
    Trace.WriteLine(msg.AsString, "Information");
    accountTransactionLoggQueue.DeleteMessage(msg);
    Trace.WriteLine(String.Format("Deleted:{0}", str), "Information");
  }
  catch (Exception ex)
  {
    Trace.WriteLine(String.Format(
      "fail to insert:{0}", str, ex.Message), "Error");
  }
}

Idempotent Capability (アイデムポテント機能)

Woodgrove Bank の顧客の 1 人が、ある口座から別の口座にお金を振り込むように要求を送信し、そのメッセージが失われたとしたらどうでしょう。顧客がメッセージを再送信すると、2 つ以上の要求がサービスに到達し、それぞれ個別に処理されることも考えられます。

Woodgrove Bank のチーム メンバーの 1 人が、このシナリオには Idempotent Capability (アイデムポテント機能) パターンが必要であるとすぐに判断しました。このパターンでは、機能や操作を安全に繰り返すことができる方法で実装することが要求されます。つまり、Woodgrove Bank が実装するソリューションには、要求ごとに一意の ID を付加し、再試行する場合は同じ ID を添えて、まったく同じメッセージを再送することを約束するクライアントが必要です。これに対処できるよう、一意 ID を Windows Azure テーブル ストレージに保存します。要求を処理する前に、その ID を持つメッセージが既に処理されたかどうかを確認する必要があります。処理済みであれば、その旨返信されますが、新しい要求に関連付けられている処理は実行されません。

このため集中管理用のデータ ストアが追加のクエリで煩わしくなりますが、これは必要なことだと考えられました。他の処理を実行できるようになるまで集中管理用のデータ ストアに格納されるクエリもあるため、結果的にパフォーマンスが低下することになります。しかし、追加の時間と他のリソースを消費するためにこれを許可することは、Woodgrove Bank の要求を満たすために妥当な選択です。

Woodgrove Bank のチームは、次のように取引 ID を追加して、IUserAccountAction の ReliableInsertMoney メソッドと ReliableWithDrawMoney メソッド、およびそれらのメソッドの実装を更新しました。

TransactionResponse ReliableInsertMoney(
  Guid userId, Guid accountId, Guid transactionId, 
  double amount, bool serializeToQueue);

TransactionResponse ReliableWithDrawMoney(
  Guid userId, Guid accountId, Guid transactionId, 
  double amount, bool serializeToQueue);

UserAccountTransaction テーブル (Windows Azure ストレージ) は、RowKey として TransactionId を追加するように更新されたため、テーブルへの各挿入には一意の取引 ID が必要になります。

次のように、一意の取引ごとに一意のメッセージ ID を送信する処理を、クライアントに設定します。

WcfClient.Using(new AccountServiceClient(), client =>{ 
  using (new OperationContextScope(client.InnerChannel)) 
  { 
    OperationContext.Current.OutgoingMessageHeaders.MessageId = 
      messageId; 
    client.ReliableInsertMoney(new AccountTransactionRequest { 
      UserId = userId, AccountId = accountId, Amount = 1000 }); 
  } 
});

ここで使用するヘルパー クラスは、soamag.com/I32/0909-4.asp にあります。

IUserAccountService 定義は、変更しませんでした。この機能の実装に必要な唯一の変更は、クライアントから送信された受信メッセージ ヘッダーから MessageId を読み取り、このメッセージを担当する処理で使用することです (図 11 参照)。

図 11 メッセージ ID の取得

public TransactionResponse ReliableInsertMoney(
  AccountTransactionRequest accountTransactionrequest) {
  var messageId = 
    OperationContext.Current.IncomingMessageHeaders.MessageId;
  Guid messageGuid = Guid.Empty;
  if (messageId.TryGetGuid(out messageGuid))
    //last parameter (true) means that we want to serialize
    //message to the queue as XML (serializeAsXml=true)
    return UserAccountHandler.ReliableInsertMoney(
      accounttransactionRequest.UserId, 
      accounttransactionRequest.AccountId, messageId, 
      accounttransactionRequest.Amount, true);
  else 
    return new TransactionResponse { StatusForTransaction = 
      Core.Types.TransactionStatus.Failed, 
      Message = "MessageId invalid" };      
}

ここで、更新された UserAccountAction が、アイデムポテントな操作ごとに取引 ID を取得します。サービスが 1 つのアイデムポテントな操作を完了しようとするときに、テーブル ストレージに取引が存在するかどうかを確認します。取引が存在すれば、サービスは AccountTransactionLogg テーブルに格納された取引のメッセージを返します。取引 ID は、RowKey として UserAccountTransaction ストレージ テーブルに保存されます。正しいユーザーとアカウントを見つけるために、サービスはパーティション キー (userid|accountid) を送信します。取引 ID が見つからない場合は、次のように、メッセージが追加処理のために AccountTransactionsQueue に格納されます。

public TransactionResponse ReliableHandleMoneyInQueueAsXml(
  UserAccountTransaction accountTransaction) {
  TransactionResponse response = this.CheckIfTransactionExists(
    accountTransaction.PartitionKey, accountTransaction.RowKey);
  if(response.StatusForTransaction == TransactionStatus.Proceed) {
    ...
  }
  return response;
}

CheckIfTransactionExists メソッド (図 12 参照) は、取引が処理されなかったことを確認するために使用します。このメソッドは、特定ユーザーの口座に対する取引 ID を検索します。取引 ID が見つかると、クライアントは、既に完了した取引の詳細を含む応答メッセージを受け取ります。

図 12 取引の状態と ID の確認

private TransactionResponse CheckIfTransactionExists(
  string userIdAccountId, string transId) {

  TransactionResponse transactionResponse = 
    new Core.Models.TransactionResponse();

  var transaction = this.TransactionExists(userIdAccountId, transId);
  if (transaction != null) {
    transactionResponse.Message = 
      String.Format("messageId:{0}, Message={1}, ", 
      transaction.RowKey, transaction.Message);
    transactionResponse.StatusForTransaction = 
      TransactionStatus.Completed;
  }
  else
    transactionResponse.StatusForTransaction = 
      TransactionStatus.Proceed;
  return transactionResponse;
}

private UserAccountTransaction TransactionExists(
  string userIdAccountId, string transId) {
  UserAccountTransaction userAccountTransaction = null;
  using (var db = new UserAccountDataContext()) {
    try {
      userAccountTransaction = 
        db.UserAccountTransactionTable.Where(
        uac => uac.PartitionKey == userIdAccountId && 
        uac.RowKey == transId).FirstOrDefault();
      userAccountTransaction.Message = "Transaction Exists";
    }
    catch (DataServiceQueryException e) {
      HttpStatusCode s;
      if (TableStorageHelpers.EvaluateException(e, out s) && 
        s == HttpStatusCode.NotFound) {
        // this would mean the entity was not found
        userAccountTransaction = null;
      }
    }
  }
  return userAccountTransaction;
}

CheckIfTransactionExists メソッドの興味深い特性として、目的のデータが見つからない場合、Windows Azure ストレージから 404 HTTP 状態コードが返されます (これは、REST インターフェイスが使用されるためです)。さらに、データが見つからない場合、ADO.NET クライアント サービス (System.Data.Services.Client) によって例外がスローされます。

詳細情報

この概念実証ソリューションの実装の詳細については、オンラインで提供するソース コードをご覧ください。SOA パターンの説明は、soapatterns.org (英語) で公開されています。ご質問があれば、herbjorn@wilhelmsen.se (英語のみ) までお問い合わせください。

Arman Kurtagić は、新しいマイクロソフト テクノロジに注力しているコンサルタントで、ビジネス駆動型の安全な IT ソリューションを提供する Omegapoint で働いています。彼は、開発者、アーキテクト、指導者、企業家などのさまざまな職務に携わってきました。また、金融、ゲーム、メディアなどの業界の経験があります。

Herbjörn Wilhelmsen は、ストックホルムに本拠を置く Forefront Consulting Group に所属するコンサルタントです。彼の主な注力分野は、サービス指向のアーキテクチャとビジネス アーキテクチャです。Wilhelmsen は、SOA Patterns Review Committee の委員長で、現在は IASA スウェーデン支部内の Business 2 IT グループのリーダーでもあります。彼は、"Prentice Hall Service-Oriented Computing Series from Thomas Erl" (英語) シリーズの『SOA with .NET and Azure』(英語) の共著者です。

Thomas Erl は、世界中でベストセラーの SOA 作家で、"Prentice Hall Service-Oriented Computing Series from Thomas Erl" (英語) シリーズの編集者、および SOA Magazine の編集者です。Erl は、SOA Systems Inc.、および SOASchool.com (英語) の SOA Certified Professional プログラムの創設者です。また、SOA Manifesto ワーキング グループの創設者で、公的イベントと民間イベントの講演者かつ指導者でもあります。詳細については、thomaserl.com (英語) をご覧ください。

この記事のレビューに協力してくれた技術スタッフの Steve Marx に心より感謝いたします。