次の方法で共有


ASP.NET のワークフロー

長時間実行処理をサポートする Web アプリケーション

Michael Kennedy

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。

  • プロセスに依存しないワークフロー
  • 同期アクティビティと非同期アクティビティ
  • ワークフロー、アクティビティ、永続化
  • ASP.NET との統合
この記事では、次のテクノロジを使用しています。
Windows Workflow Foundation、ASP.NET

目次

ワークフローの活用
同期アクティビティと非同期アクティビティ
アイドル状態とは何か
同期タスクの非同期化
ワークフローとアクティビティ
永続化
実現に向けて
ASP.NET との統合
考慮すべきこと
まとめ

長時間実行処理のサポートは、ソフトウェア開発者が Web アプリケーションを構築する際にたびたび直面する課題の 1 つです。たとえば、オンライン ストアの清算処理は、完了までに数分を要することも少なくありません。どの程度の時間をもって "長時間" と見なすかはケース バイ ケースですが、この記事で取り上げるのは、もっとスケールの大きい長時間実行処理です。処理が完了するまでに数日から数週間、場合によっては数か月に及ぶケースを扱います。その代表的な例が人材採用アプリケーションです。そのプロセスには、複数のユーザー間の対話処理や、さまざまな書類のやり取りが伴います。

最初は、ASP.NET の観点で比較的取り組みやすい問題から考えてみましょう。自分がオンライン ストアの清算処理のためのソリューションを構築することになったと仮定してください。清算処理に要する時間を考えると、このソリューションには特別に考慮しなければならない事柄があります。たとえば、ショッピング カートのデータはどこに保存すればよいでしょうか。まず考えられるのが、ASP.NET のセッションです。場合によっては、サイトの更新や負荷分散まで視野に入れて、セッション状態をプロセス外の状態サーバーやデータベースに移動することもできます。それでも、必要なツールはすべて ASP.NET そのものに備わっているので解決は難しくありません。

しかし、処理の持続時間が ASP.NET セッションの標準的な持続時間 (20 分) を越える場合や、先ほど挙げた人材採用アプリケーションのように、処理にかかわるユーザーが複数存在する場合はどうでしょうか。残念ながら、そこまでの機能を ASP.NET に求めることはできません。ASP.NET のワーカー プロセスはアイドル時に自動的にシャットダウンし、定期的に自分自身をリサイクルする、ということを思い出してください。これは、プロセス内に保持されている状態が失われることを意味し、長時間実行処理の実現にとって大きな問題です。

このように、きわめて長時間にわたって続く処理を単一のプロセス内でホストすることを想像してみてください。既に述べた理由から、ASP.NET のワーカー プロセスで、そのようなことを実現しようとすることには、明らかに無理があります。だとすれば、この処理を実行するためだけの専用の Windows サービスを作成するというのはどうでしょうか。サービスを再起動しないという前提であれば、ASP.NET を直接使用する方法よりは現実的です。理論上、そのサービス プロセスが決して自動的に再起動しないのであれば、長時間実行処理の状態が失われる心配もありません。

しかし、それで本当に問題が解決するでしょうか。おそらく無理でしょう。サーバーを負荷分散することになったらどうしたらよいでしょうか。単一のプロセスで解決を図ることはきわめて困難です。しかも、サーバーを再起動しなければならなくなったり、プロセスがクラッシュすることも考えられます。そのようなことが起これば、実行中の処理がすべて失われることになります。

実際には、処理の完了に数日から数週間かかるような場合、プロセスのライフサイクルに依存しないソリューションが必要です。あらゆるアプリケーションに共通することではありますが、ASP.NET Web アプリケーションではこのことが特に重要となります。

ワークフローの活用

Web アプリケーションを構築するためのテクノロジの話をしているのに、なぜ Windows Workflow Foundation (WF) が、と意外に思う方もいらっしゃるかもしれません。しかし、WF には、ワークフロー ソリューションとして注目に値する機能がいくつか備わっています。たとえば、アイドル状態のワークフローをプロセス空間から完全にアンロードしたり、アイドル状態ではなくなったワークフローを自動的にアクティブなプロセスへと再ロードしたりすることができるため、プロセスに依存しない長時間実行処理が可能です (図 1 を参照)。WF を使用することにより、ASP.NET ワーカー プロセスのライフサイクルが持つ不確定性を克服し、Web アプリケーション内での長時間実行処理を実現することが可能となります。

図 1 ワークフローが複数のプロセス インスタンス間で処理を維持する

これは、WF が持つ 2 つの主要な機能が組み合わさって実現されています。1 つ目は、非同期のアクティビティから送信される信号です。ワークフローがアイドル状態で外部イベントを待機していると、ワークフロー ランタイムに対して、そのことを伝える信号が送信されます。2 つ目は、永続化サービスです。アイドル状態のワークフローをプロセスからアンロードし、データベースなどの持続性のある格納場所に保存したうえで、再び実行できる状態になった段階で、そのワークフローを再度読み込むものです。

プロセスに依存しないという特性には、別のメリットもあります。持続性を確保できるだけでなく、負荷分散が容易になるという点です。プロセスやサーバーの障害への耐性 (フォールト トレランス) を実現することが可能です。

同期アクティビティと非同期アクティビティ

アクティビティとは、WF において、それ以上、分割できない不可分な要素をいいます。すべてのワークフローは、アクティビティからコンポジット デザイン パターンに似たパターンで構築されます。事実、ワークフローそのものは、目的に特化して構築されたアクティビティにすぎません。これらのアクティビティは、同期アクティビティと非同期アクティビティに大別されます。同期アクティビティの特徴は、そのすべての命令が、最初から最後まで実行されることです。

同期アクティビティの例としては、オンライン ストアにおいて、顧客から受けた注文の税金計算を行うアクティビティが考えられます。では、このようなアクティビティの具体的な実装方法を考えてみましょう。通常の WF アクティビティと同様、作業の大半は、オーバーライドされた Execute メソッドで行うことになるかと思います。このメソッドは、おそらく次のようなステップで進行するのではないでしょうか。

  1. 直前のアクティビティから注文データを取得します。通常、この処理はデータ バインドを介して行います。実際の例については、後ほど説明します。
  2. 注文に関連付けられている顧客をデータベースから検索します。
  3. 顧客の場所に基づく税率をデータベースから検索します。
  4. 注文に関連付けられている税率や商品をもとに単純な算術計算を実行します。
  5. 税額の合計をプロパティとして保存します。後続のアクティビティは、このプロパティにバインドすることによって、清算処理を実行します。
  6. ワークフロー ランタイムに対して、このアクティビティが完了したことを伝える信号を送ります。これは、Execute メソッドから "完了" ステータス フラグを返すことによって行われます。

ここで大切なポイントは、同期アクティビティには待機時間が存在せず、常に何かの処理が実行されているということです。Execute メソッドはただ、ステップを上から順に実行し、完了していくだけです。これは同期アクティビティの基本的な特性です。すべての処理は Execute メソッドで実行されます。

非同期アクティビティは、それとは異なります。同期アクティビティとは異なり、ある程度の時間、実行した後、外的作用の発生を待機します。待機中、アクティビティはアイドル状態となります。特定のイベントが発生すると、アクティビティの処理が再開されて、必要な処理が完了します。

人材採用プロセスで、特定の職種への応募書類をマネージャが審査するとき、このステップを非同期アクティビティと捉えることができます。このマネージャが休暇中で翌週まで応募書類に目を通すことができないとしたらどうなるでしょうか。マネージャからの返答を待つ間、Execute メソッドの途中で処理がブロック状態に陥ってしまうとしたら、決して合理的とは言えません。ソフトウェアが人間の介入を待つと、それだけ待ち時間が増えることになります。この点は、開発者が設計時に考慮する必要があります。

アイドル状態とは何か

ここで、少し言葉の意味を整理しておきましょう。"アイドル状態になる" とはどのような意味でしょうか。

以下の例は、Web サービスを使用してパスワードを変更するクラスのコードです。

public class PasswordOperation : Operation {
  Status ChangePassword(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    // This can take up to 20 sec for 
    // the web server to respond:
    bool result = svc.ChangePassword( userId, pw );

    Logger.AccountAction( "User {0} changed pw ({1}).",
      userId, result);
    return Status.Completed;
  }
}

ChangePassword メソッドがアイドル状態に陥ることはあるでしょうか。あるとすれば、それはどの部分でしょうか。

このメソッドのスレッドは、UserService からの HTTP 応答を待機する間、ブロック状態になります。したがって、理論上はイエスです。つまり、サービスの応答を待機する間、スレッドはアイドル状態になります。しかし、サービスからの応答を待つ間、スレッドに他の作業を行わせることはできません。スレッドは既に利用されているため、現実には、そのようなことは不可能です。したがって、WF の観点から言えば、この "ワークフロー" は決してアイドル状態になることはありません。

アイドル状態にならないと、なぜ言えるのでしょうか。たとえば、ChangePassword のような処理を効率的に実行するための、より大規模なスケジューラ クラスがあったとします。ここでの "効率的" とは、複数の処理を並列的に実行したり、完全な並列処理を最小限のスレッド数で実現したりすることです。このような効率性を実現するには、処理がいつ実行され、いつアイドル状態になるかのタイミングを把握することが鍵となります。現在実行中のスレッドで処理がアイドル状態になると、スケジューラは、処理を再開する準備が整うまでの間、そのスレッドを他の作業に当てることができます。

残念ながら、スケジューラからは ChangePassword メソッドの内部が見えません。実質的にアイドル状態になる時間が存在したとしても、外から、つまり、スケジューラから見ると、このメソッドは、中断の許されない 1 つの処理単位なのです。この処理単位を分割し、スレッドのアイドル時間を再利用するような能力は、スケジューラにはありません。

同期タスクの非同期化

処理を 2 つに分割することによって、処理に透明性を持たせ、効率的なスケジューリングを実現することが可能です。たとえば、アイドル状態になるまでの部分と、アイドル状態から抜けた後のコードを実行する部分に分けます。

冒頭では、一定の前提条件に基づいて、Web サービスを利用する可能性について触れました。その前提条件の下、Web サービス プロキシそのものが持つ非同期機能を利用することもできます。ただし、それは、あくまで単純化した例です。以降の説明を読むとわかるように、WF でのアプローチは、それとは若干異なります。

図 2 は、先ほどのパスワード変更メソッドを改良したものです (ChangePasswordImproved)。前回と同様、このメソッドでは、まず Web サービス プロキシを作成します。次に、サーバーから応答が返されたときに通知を受けるためのコールバック メソッドを登録します。その後、サービスを非同期的に呼び出し、Status.Executing を返します。スケジューラは、これをもって処理がアイドル状態に移行したこと、また、処理そのものは未完了である旨を把握できます。これは重要なステップです。このステップがあることで、コードがアイドル状態のとき、スケジューラは他の作業を実行することができます。最後に、"完了" イベントが発生したら、スケジューラに処理の完了を知らせ、次の処理に進むことができるようにします。

図 2 単純なパスワード変更サービス呼び出し

public class PasswordOperation : Operation {
  Status ChangePasswordImproved(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    svc.ChangePasswordComplete += svc_ChangeComplete;
    svc.ChangePasswordAsync( userId, pw );
    return Status.Executing;
  }

  void svc_ChangeComplete(object sender, PasswordArgs e) {
    Logger.AccountAction( "User {0} changed pw ({1}).",
      e.UserID, e.Result );

    Scheduler.SignalCompleted( this );
  }
}

ワークフローとアクティビティ

今度は、処理がアイドル状態になるという概念を WF のアクティビティに当てはめてみたいと思います。既に見てきた内容とよく似ていますが、これまでと違うのは、それを WF のモデル内で行う必要があるという点です。

WF には、組み込みのアクティビティが数多く付属しています。しかし、現実に WF を使用してシステムを構築しようとすると、すぐに再利用可能な独自のアクティビティを作成したくなると思います。その方法は簡単です。汎用性のある Activity クラスを継承して独自のクラスとして定義すればよいのです。次に、基本的な例を示します。

class MyActivity : Activity {
  override ActivityExecutionStatus 
    Execute(ActivityExecutionContext ctx) {

    // Do work here.
    return ActivityExecutionStatus.Closed;
  }
}

カスタム アクティビティで何かの処理を実行するためには、Execute メソッドをオーバーライドする必要があります。有効期間の短い同期アクティビティであれば、必要な作業は、このメソッドにアクティビティの処理を実装し、Closed ステータスを返すだけです。

実際のアクティビティには、それ以外にも考慮しなければならない問題がいくつかあります。たとえば、同じワークフロー内の他のアクティビティとの通信や、ワークフローをホストしている、より大きなアプリケーションとの通信はどのように実現すればよいでしょうか。データベース システムや UI のインタラクションなど、各種のサービスには、どのようにしてアクセスすればよいでしょうか。ただ、いずれの問題も、同期アクティビティを構築する場合は比較的単純です。

一方、非同期アクティビティを構築する場合、問題はもっと複雑です。さいわい、ほとんどの非同期アクティビティに共通して採用できるパターンがあります。基本クラスにこのパターンを取り入れるのは簡単です。以降、その方法を紹介します。

以下の手順は、一般的な非同期アクティビティを構築するために必要な基本手順をまとめたものです。

  1. Activity の派生クラスを作成します。
  2. Execute メソッドをオーバーライドします。
  3. 待機中の非同期イベントの完了通知を受信するためのワークフロー キューを作成します。
  4. キューの QueueItemAvailable イベントをサブスクライブします。
  5. 長時間実行処理を開始します (特定の職種に対する応募書類の審査をマネージャに電子メールで依頼するなど)。
  6. 外部イベントの発生を待機します。実質的に、これはアクティビティがアイドル状態になったという信号です。この信号は、ExecutionActivityStatus.Executing を返すことによってワークフロー ランタイムに伝えます。
  7. イベントが発生すると、QueueItemAvailable イベントを処理するメソッドが、キューからアイテムを削除し、それを必要なデータ型に変換して、結果を処理します。
  8. 通常は、これでアクティビティの処理は完了です。処理の完了は、ActivityExecutionContext.CloseActivity を返すことによって、ワークフロー ランタイムに伝えられます。

永続化

この記事の冒頭で、プロセスに依存しない長時間実行処理をワークフローで実現するためには、非同期アクティビティと永続化サービスという、2 つの基本的な機能が不可欠であると書きました。これまで取り上げてきたのは、そのうちの一方の非同期アクティビティです。以降は、永続化を支える "ワークフロー サービス" テクノロジについて深く掘り下げていくことにします。

ワークフロー サービスは、WF の拡張性の鍵となる部分です。WF ランタイムの実体はクラスであり、このクラスがアプリケーション内でインスタンス化されることによって、実行中のすべてのワークフローがホストされます。このクラスには、ワークフロー サービスの概念によって初めて両立される、相反する 2 つの設計目標があります。このワークフロー ランタイムの 1 つ目の目標は、あらゆる場面で使用できるように、オブジェクトを軽量化することです。2 つ目は、実行中のワークフローに対して、強力な機能を提供することです。たとえば、アイドル状態のワークフローを自動的に永続化したり、ワークフローの進捗状況を追跡したりする機能のほか、各種のカスタム機能をサポートすることなどが挙げられます。

実際に組み込まれている機能は、その一部だけであるため、ワークフロー ランタイムそのものはきわめて軽量です。永続化や追跡といった、より複雑なサービスは、サービス モデルを介し、必要に応じてインストールすることになります。実際、ここでいうサービスとは、ワークフローに追加する、あらゆる機能を指します。WorkflowRuntime クラスの AddService メソッドを呼び出すだけで、これらのサービスをランタイムにインストールすることができます。

void AddService(object service)

AddService が引数として受け取るのは System.Object への参照です。そのため、ワークフローに必要なあらゆる機能を追加することが可能です。

ここでは、2 つのサービスを使用します。1 つ目は、非同期アクティビティの構築に不可欠なワークフロー キューアクセスするための WorkflowQueuingService です。このサービスは既定でインストールされており、カスタマイズすることはできません。もう 1 つは、SqlWorkflowPersistenceService サービスです。これは文字通り、永続化の機能を提供するサービスです。既定ではインストールされていません。さいわい、このサービスは WF に付属しています。必要なことは、このサービスをランタイムに追加するだけです。

SqlWorkflowPersistenceService という名前からもわかるように、このサービスにはデータベースが必要となります。このサービス用に空のデータベースを作成するか、既存のデータベースにいくつかのテーブルを追加してもかまいません。私個人としては、ワークフローの永続化データと他のデータを混在させるよりも、専用のデータベースを使用した方がよいと考えます。そのため、ここでは、SQL Server に空のデータベースを作成することにしました。データベースの名前は WF_Persist です。必要なデータベース スキーマおよびストアド プロシージャは、いくつかのスクリプトを実行することによって作成します。これらは、Microsoft .NET Framework の一部としてインストールされ、既定では、次のフォルダに格納されます。

C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\

手順としては、最初に SqlPersistenceService_Schema.sql スクリプトを実行し、次に SqlPersistenceService_Logic.sql スクリプトを実行することになります。後は、永続化サービスに接続文字列を渡すことによって、このデータベースを使った永続化が可能となります。

SqlWorkflowPersistenceService sqlSvc = 
    new SqlWorkflowPersistenceService(
  @"server=.;database=WF_Persist;trusted_connection=true",
  true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));

wfRuntime.AddService(sqlSvc);

このように、ただ AddService メソッドを呼び出すだけで、アイドル状態のワークフローをアンロードしてデータベースに格納したり、必要になったときに、再び復元したりできるようになります。後は WF ランタイムがすべてを引き受けて処理します。

実現に向けて

以上の点を踏まえておけば、技術的な基本知識は十分でしょう。後は、その知識をつなぎ合わせることにより、長時間実行処理をサポートする ASP.NET Web サイトを作成できます。この例で取り上げる内容は、大きく 3 つに分けることができます。非同期アクティビティの構築、ワークフロー ランタイムの Web アプリケーションへの統合、そして、Web ページとワークフロー間の通信です。

Trey Research 社という架空の .NET コンサルティング会社を例に説明していこうと思います。同社は、コンサルタントの人材募集と人材採用のプロセスを自動化したいと考えています。そこで、この人材採用プロセスを支援する ASP.NET Web サイトを構築することになったという設定です。できるだけ単純化するように努めていますが、このプロセスには、次のように複数のステップが含まれています。

  1. 求職者が Trey Research 社の Web サイトにアクセスし、仕事が欲しいという意思表示をします。
  2. 新たに応募があった旨の電子メールがマネージャに送信されます。
  3. このマネージャが応募書類を審査し、その応募者を特定の職種に関して承認します。
  4. 提示された職種についての電子メールが応募者に送信されます。
  5. 応募者が Web サイトにアクセスし、その職種に同意するか拒否します。

単純なプロセスではありますが、相手からの働きかけに対して返事をしたり、より詳しい情報を入力したりするなど、待ち状態になる部分が存在します。こうしたアイドル ポイントには、ある程度の時間がかかることが予想されます。したがって、長時間実行処理のデモンストレーションとしては最適な事例です。

この Web アプリケーションは、記事のソース コードに含まれています。ただし、その効果をフルに体験するためにも、永続化データベースを作成し、実際にサンプルを構成して使用することをお勧めします。永続化サービスの有効と無効を管理するためのスイッチを設けてありますが、既定では無効になっています。有効にするには、web.config の AppSettings セクションで、usePersistDB を true に設定してください。私の Web サイトにサンプルが置いてありますので、必要に応じてご覧ください。

fig03.gif

図 3 ワークフローとしての人材採用プロセス

まずは、ASP.NET に一切依存しないワークフローの設計から始めることにしましょう。ワークフローを構築するために、4 つのカスタム アクティビティを作成します。1 つ目は、電子メールの送信アクティビティです。これは単純な同期アクティビティです。それ以外の 3 つは、それぞれ、前述のステップ 1、3、5 に相当する非同期アクティビティです。長時間実行処理の実現にとってきわめて重要なアクティビティです。この 3 つのアクティビティをそれぞれ、GatherEmployeeInfoActivity、AssignJobActivity、および ConfirmJobActivity と呼ぶことにします。図 3 に示した骨組みだけのワークフローに対して、これらのアクティビティを当てはめながら肉付けしていこうと思います。

電子メールの送信は単純なアクティビティなので、この記事では詳しく解説しません。前に述べた MyActivity クラスと同様の同期アクティビティです。詳細については、コードをダウンロードして確認してください。

これで残った課題は、3 つの非同期アクティビティの作成のみとなりました。非同期アクティビティを構築するための 8 つのステップから成るプロセスについては、既に説明したとおりです。これらを共通の基本クラスとしてカプセル化できれば、必要な手間を大幅に減らすことができます。最終的な目標は、AsyncActivity というクラスを定義することです (図 4 を参照)。ここに掲載されている一連のコードには、実際のコードにある内部的なヘルパー メソッドやエラー処理が省略されています。できるだけ単純化して説明するための配慮です。ご了承ください。

図 4 AsyncActivity

public abstract class AsyncActivity : Activity {
  private string queueName;

  protected AsyncActivity(string queueName) {
    this.queueName = queueName;
  }

  protected WorkflowQueue GetQueue(
      ActivityExecutionContext ctx) {
    var svc = ctx.GetService<WorkflowQueuingService>();
    if (!svc.Exists(queueName))
      return svc.CreateWorkflowQueue(queueName, false);

    return svc.GetWorkflowQueue(queueName);
  }

  protected void SubscribeToItemAvailable(
      ActivityExecutionContext ctx) {
    GetQueue(ctx).QueueItemAvailable += queueItemAvailable;
  }

  private void queueItemAvailable(
      object sender, QueueEventArgs e) {
    ActivityExecutionContext ctx = 
      (ActivityExecutionContext)sender;
    try { OnQueueItemAvailable(ctx); } 
    finally { ctx.CloseActivity(); }
  }

  protected abstract void OnQueueItemAvailable(
    ActivityExecutionContext ctx);
}

この基本クラスをよく見ると、非同期アクティビティを構築するための定型的な処理がいくつかひとまとめにされていることがわかります。最初から最後までざっと目を通してみましょう。最初に、キュー名を文字列として受け取るコンストラクタがあります。ワークフロー キューは、ホスト アプリケーション (Web ページ) からの入力を受け取るポイントです。ホスト アプリケーションは、このキューを介すことで、疎結合のアーキテクチャを維持しながら、データをアクティビティに渡すことができます。これらのキューは、名前とワークフロー インスタンスで参照されるため、すべての非同期アクティビティには固有のキュー名が必要です。

その次に定義されているのは、GetQueue メソッドです。ご覧のように、ワークフロー キューにアクセスしたり、ワークフロー キューを作成したりするのは簡単ですが、その都度、コーディングするのは面倒です。そこで、GetQueue メソッドを、このクラス (またはその派生クラス) 内でのみ使用するためのヘルパー メソッドとして作成しました。

次に、SubscribeToItemAvailable というメソッドが定義されています。このメソッドには、ワークフロー キューにアイテムが到着したときに発生するイベントをサブスクライブする処理がカプセル化されています。このイベントの発生は、ほとんどの場合、長く続いた待機時間 (ワークフローがアイドル状態になる期間) の終わりを意味します。したがって、具体的な処理の流れは、次のようになるかと思います。

  1. 長時間実行処理を開始し、SubscribeToItemAvailable を呼び出します。
  2. アクティビティがアイドル状態であることをワークフロー ランタイムに伝えます。
  3. ワークフロー インスタンスが永続化サービスによってシリアル化されてデータベースに格納されます。
  4. 処理が完了すると、アイテムがワークフロー キューに送られます。
  5. これによって、データベースからワークフロー インスタンスを復元する処理がトリガされます。
  6. 基本クラス AsyncActivity によって、抽象テンプレート メソッド OnQueueItemAvailable が実行されます。
  7. アクティビティの処理が完了します。

この AsyncActivity クラスの実際の働きを確認するために、AssignJobActivity クラスを実装してみましょう。他の 2 つの非同期アクティビティも要領は同じです。コード サンプルにも同梱されています。

図 5 (AssignJobActivity) を見てください。基本クラス AsyncActivity に用意されているテンプレートが使用されています。長時間にわたるアクティビティを開始する前の、事前処理を実行するため、Execute をオーバーライドしています。次に、新しいデータが到着したときに発生するイベントをサブスクライブします。

図 5 AssignJobActivity

public partial class AssignJobActivity : AsyncActivity {
  public const string QUEUE NAME = "AssignJobQueue";

  public AssignJobActivity()
    : base(QUEUE_NAME) 
  {
    InitializeComponent();
  }

  protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext ctx) {
    // Runs before idle period:
    SubscribeToItemAvailable(ctx);
    return ActivityExecutionStatus.Executing;
  }

  protected override void OnQueueItemAvailable(
      ActivityExecutionContext ctx) {
    // Runs after idle period:
    Job job = (Job)GetQueue(ctx).Dequeue();

    // Assign job to employee, save in DB.
    Employee employee = Database.FindEmployee(this.WorkflowInstanceId);
    employee.Job = job.JobTitle;
    employee.Salary = job.Salary;
  }
}

このコードは、"ホスト アプリケーション (Web ページ) がマネージャから職種情報を収集した段階で、新しい Job オブジェクトをアクティビティのキューに送信する" という暗黙的な決まり事に基づいて作成されています。これをもって、アクティビティは処理を続行できると判断し、データベース内の被雇用者データ (employee) を更新します。後続のワークフロー アクティビティは、提示された職種を伝える電子メールを応募者に送信します。

ASP.NET との統合

ワークフロー内部のしくみは以上です。しかし、ワークフローを開始するにはどうすればよいのでしょうか。Web ページは、実際、どのようにして、マネージャから職種情報を収集するのでしょうか。職種情報 (Job オブジェクト) をアクティビティに渡す方法についても疑問が残ります。

以上の点について順に説明していくことにしましょう。まずは、ワークフローの開始方法についてです。Web サイトのランディング ページには、"Apply Now" という見出しに続けてリンクが表示されています。応募者がこのリンクをクリックすると、ワークフローと、ユーザー インターフェイスのナビゲーションの両方が同時に開始されます。

protected void LinkButtonJoin_Click(
    object sender, EventArgs e) {
  WorkflowInstance wfInst = 
    Global.WorkflowRuntime.CreateWorkflow(typeof(MainWorkflow));

  wfInst.Start();
  Response.Redirect(
    "GatherEmployeeData.aspx?id=" + wfInst.InstanceId);
}

単にワークフロー ランタイムの CreateWorkflow を呼び出し、ワークフロー インスタンスを開始しています。以降、このワークフロー インスタンスを追跡するために、後続のすべての Web ページには、対応するインスタンス ID をクエリ パラメータとして渡すことになります。

Web ページからワークフローに再度データを戻すには、どうすればよいでしょうか。図 6 の AssignJobPage クラスを見てください。マネージャが応募者の職種を選ぶ際の処理が実装されています。

図 6 職種の割り当て

public class AssignJobPage : System.Web.UI.Page {
  /* Some details omitted */
  void ButtonSubmit_Click(object sender, EventArgs e) {
    Guid id = QueryStringData.GetWorkflowId();
    WorkflowInstance wfInst = Global.WorkflowRuntime.GetWorkflow(id);

    Job job = new Job();
    job.JobTitle = DropDownListJob.SelectedValue;
    job.Salary = Convert.ToDouble(TextBoxSalary.Text);

    wfInst.EnqueueItem(AssignJobActivity.QUEUE_NAME, job, null, null);

    buttonSubmit.Enabled = false;
    LabelMessage.Text = "Email sent to new recruit.";
  }
}

職種の割り当てを行う Web ページは、単純な入力フォームです。選択可能な職種を表示するドロップダウン リストと、給与の提示額を入力するためのテキスト ボックスがあります。また、ここでは省略されていますが、現在の応募者も表示されるようになっています。マネージャが応募者に対して職種と給与額を割り当て、送信ボタンをクリックすると、図 6 に示したコードが実行されます。

このページでは、ワークフロー インスタンス ID をクエリ文字列パラメータに使用して、関連するワークフロー インスタンスを検索します。すると、フォームに入力された値に基づいて、Job オブジェクトが作成され、初期化されます。最後に、Job オブジェクトをアクティビティ キューに追加して、この情報をアクティビティに送り返します。これは、アイドル状態のワークフローを再度読み込んで、処理を続行できるようにする非常に重要なステップです。AssignJobActivity は、この Job オブジェクトを、収集済みの被雇用者データ (employee) に関連付けて、データベースに保存します。

この 2 つのコード例は、非同期アクティビティの実現と、ワークフローと外部ホストとの通信にとって、ワークフロー キューがいかに重要な役割を果たしているかを物語っています。また、ここでのワークフローの使用はページ フローに一切影響を及ぼさない点にも注目してください。WF を使用してページ フローを制御することもできますが、この記事の本筋からそれるので、ここでは使用していません。

図 6 を見ると、ワークフロー ランタイムに対し、グローバル アプリケーション クラスを介してアクセスしていることがおわかりいただけると思います。次に示した部分です。

WorkflowInstance wfInst = 
  Global.WorkflowRuntime.GetWorkflow(id);

これは、Web アプリケーションに Windows Workflow を統合する作業の仕上げとなる部分です。すなわち、すべてのワークフローは、ワークフロー ランタイム内で実行されるという事実です。AppDomain で実行できるワークフロー ランタイムの数に制限はありませんが、通常は、ワークフロー ランタイムが 1 つあれば十分です。加えて、WF ランタイム オブジェクトはスレッド セーフであるため、ここでは WorkflowRuntime をグローバル アプリケーション クラスの public static プロパティとして宣言しています。さらに、WorkflowRuntime をアプリケーションの起動イベントで開始し、アプリケーションの終了イベントで停止しています。図 7 は、グローバル アプリケーション クラスからの抜粋です。

図 7 ワークフロー ランタイムの開始

public class Global : HttpApplication {
  public static WorkflowRuntime WorkflowRuntime { get; set; }

  protected void Application_Start(object sender, EventArgs e) {
    WorkflowRuntime = new WorkflowRuntime();
    InstallPersistenceService();
    WorkflowRuntime.StartRuntime();
    // ...
  }

  protected void Application_End(object sender, EventArgs e) {
    WorkflowRuntime.StopRuntime();
    WorkflowRuntime.Dispose();
  }

  void InstallPersistenceService() {
    // Code from listing 4.
  }
}

コード例では、アプリケーションの起動イベントで、ランタイムを作成し、永続化サービスをインストールして、ランタイムを開始しています。同様に、アプリケーションの終了イベントで、ランタイムを停止しています。これは重要な手順です。実行中のワークフローが存在する場合は、それらのワークフローがアンロードされるまでブロック状態になります。ランタイムを停止した後で、Dispose を呼び出している点に注意してください。StopRuntime を呼び出した後、Dispose を呼び出すのは、重複する処理のように見えますが、そうではありません。両方のメソッドをこの順序で呼び出す必要があります。

考慮すべきこと

ここでいくつかの事柄を補足したいと思います。これまではっきりとは言及してこなかった部分です。まずは、ManualWorkflowSchedulerService を使用しなかった理由を説明します。通常、WF と ASP.NET を連携させる場合、ワークフローの既定のスケジューラ (スレッド プールを使用するもの) を、ManualWorkflowSchedulerService と呼ばれるサービスに置き換えるのが一般的です。今回、それを使用しなかったのは、この記事の本題である長時間実行処理にとって適さない、または必要ないと判断したためです。ManualWorkflowSchedulerService は、特定のリクエスト内で完結する単一のワークフローを実行するような場合には有効な手段です。しかし、ワークフローがプロセスの有効期間を越えて存在するような場合は、適切な手段とは言えません。1 回のリクエストで完結しないケースではなおさらです。

次は、特定のワークフロー インスタンスに関して、最新の進捗状況を追跡することが可能かどうか、という点についてです。結論から言えば可能です。WF には完全な追跡サービスが備わっており、SQL の永続化サービスと同様の要領で使用できます。詳細については、Matt Milner が執筆した 2007 年 3 月の「基礎」コラム、「Windows Workflow Foundation の追跡サービス」を参照してください。

まとめ

この記事で解説してきた手法を簡単にまとめてみましょう。まず、きわめて長時間実行される処理には ASP.NET のワーカー プロセスやプロセス モデルが適さない理由を概説しました。この制限を克服するために私が利用したのは、WF が持つ 2 つの機能、つまり、非同期アクティビティとワークフローの永続化です。この 2 つの機能を組み合わせることによって、プロセスに依存しない処理を実現しました。

非同期アクティビティの構築には、いくぶん込み入った処理も伴います。そのため、こまごまとした部分は AsyncActivity という基本クラスにカプセル化することにしました。具体的なコードは、この記事でも紹介したとおりです。次に、長時間実行処理を、非同期アクティビティから成るシーケンシャル ワークフローとして表現し、それを Web アプリケーションに統合することで、プロセス非依存を簡単に実現できることを証明しました。

最後に、そのワークフローを ASP.NET に統合する手順を紹介しました。これは、大きく 2 つの部分に分けることができます。つまり、ワークフロー キューを介してアクティビティと通信する部分と、グローバル アプリケーション クラスでランタイムをホストする部分です。

WF と ASP.NET の統合によって長時間実行処理を実現する手法をひととおり読んだ皆さんは、.NET Framework 上にソリューションを構築するための有益な引き出しをまた 1 つ増やしたことになります。

Michael Kennedy は、DevelopMentor でインストラクタを務めており、.NET のコア テクノロジのほか、アジャイル開発や TDD 開発手法などを専門としています。Michael の Web サイトおよびブログには、michaelckennedy.net でアクセスできます。