内部エラー (WinWF Internal Error) 発生を想定したSharePoint ワークフローをデザインする

こんにちは SharePoint サポートの森 健吾 (kenmori) です。今回の投稿では、内部エラー (WinWF Internal Error) 発生を想定したSharePoint 2007 および 2010 におけるワークフローをデザインする方法をお伝えします。

少し古くなってしまいますが、前回の投稿 (https://blogs.technet.com/b/sharepoint_support/archive/2011/01/12/sharepoint.aspx) と合わせて、ワークフローの導入前や導入後においても問題を最小限に抑えるためにご確認いただきたい内容をまとめています。

 

導入 : アプリケーションの内部エラーという宿命

SharePoint ワークフローも、他のアプリケーションと同様内部エラーが発生することがあります。

アプリケーション開発者が、アプリケーションで予期せぬエラーが発生することを全て予期し完全に避けるようコーディングすることは不可能です。そのため、一般的にアプリケーション開発者は、例外処理として該当コードブロック内でエラー発生時にカスタム処理を実装して、予期せぬエラーが発生した場合はログを記載したり、例えばエラーを管理者に通知したり、場合によってはリトライしたりする実装を行います。

C# 例外処理の例

----

try
{
    // 処理
}
catch(Exception ex)
{
    // 例外処理
}

---- 

事前に認識しておくべき点として、SharePoint ワークフローは標準機能では診断ログにエラー内容を記録するよう留めています。様々な種類のワークフローが動作する基盤を提供しており、ランタイム側は各処理の重要度がどのようなものかや、エラーの種類 (入力ミスなどを含む) を認識できませんのでやむを得ません。このことから、 Visual Studio 等でカスタマイズ (FaultHandler などを定義) しない限り、標準機能の範囲でエラーを捕捉して独自処理 (特定のアドレスに通知) を実装したり、リトライしたりということはできません。

ただし、エラー終了してしまうことはシステム上やむを得ないとしても、ビジネス フローが止まってしまい、それに気づかずに業務が遅延または処理されないなど影響を与えてしまうことは、業務上あってはならないことです。

SharePoint ワークフローでは、このようにエラーとなったワークフローに対する対処策としては、エラーの原因が入力ミスの場合等を除き、速やかに一旦終了した上で再度開始させるのみとなります。このような問題解決に対する対処をあらかじめ想定しておくことは、ワークフローを使用する上で不可欠になると考えています。
また、多くのユーザーの間で、Visual Studio を使用しない範囲でワークフローを使用するにとどめていることも認識しており、そのような条件下においてエラー発生時をシミュレーションした現実的な対処策を練っていく必要があると認識しています。

今回の投稿では、その対処策として考えられるベスト プラクティスとしての一案を共有させていただきたいと考えています。

 

エラーを想定したワークフロー テンプレートの実装方法

このセクションでは、ワークフローにて発生し得るエラーを想定した上で、実際のワークフローの作成方法についての推奨内容を確認します。

1.     ワークフローを分割する

上述の通りワークフローが失敗した場合には、該当のワークフローを一度終了して、再度開始する必要があります。このため、複数人による承認プロセスが存在する場合は、最初の人からやり直しとなり、承認者の二度手間になるだけでなく、ビジネス プロセスに遅延が生じます。


図 1  : 1 つのワークフローで複数の承認プロセスを進める際のイメージ

複数人による承認を必要とするワークフローを実装する場合、人数分にワークフローを分割した方が得策です。その場合、ワークフローがエラー終了したとしても、再度開始した際にはワークフローの最初から全体のプロセスが再度開始します。


図 2 : ワークフローを承認プロセスごとに分割した際のイメージ

これに対し、ワークフローを上図のように分散 (ワークフロー A をワークフロー A, B, C に分散) して、それぞれのワークフローが順番に起動するように実装した場合、それぞれのワークフロー内でエラーが発生した場合にはそれぞれのワークフローの最初から開始する動作となります。そのため、エラー発生時のリスクが減ります。

なお、具体的に上記のようにワークフローを連鎖させるような実装方法例として、1) 各ワークフローの条件で動作が必要な際は抜ける方法 (下記図 3 参照のこと) や、2) サードパーティ製のアクションを使用してワークフローを直接指定して起動する方法 (またはそれを開発する方法) などが考えられます。

また、ワークフローの進捗状況を管理するために、アイテムに列などを追加し、どこまで承認されたかをワークフローの処理側で記録することも合わせて必要となります。

補足 : 競合の保存について
ワークフローからアイテムを更新する際には、UI 側の処理と同じタイミングで実施すると更新の競合が発生する場合がありますので、事前に待機処理を加える必要も考慮する必要があります。
更新の競合は、ユーザーの入力内容をを保護する目的で実装されています。ユーザーの更新操作とワークフローの処理が同時にアイテムを更新すると、ユーザーの入力内容が直後のワークフローによってそのまま更新され消失してしまいます。ワークフローは、この状況を防ぐために更新前後の内部バージョン値を使用して更新時に競合を検出し、エラーを発生させます。

 
図 3 : ワークフローで条件を使用する

 

2. ワークフローが行ったアイテムの更新処理はロールバックされない

ワークフローの再実行を考慮した設計を行う際に、予め認識しておく事項としてワークフローがエラーになった際に、それまでアイテム等に実施された内容はロールバックされない点にあります。

例えば、ワークフローに条件が追加されていて処理を分岐している場合、初期状態ではないと判断すれば、初回実行と 2 回目の実行では動作が異なる結果に至ります。

このような再実行の可能性を考慮してワークフローは作成される必要があります。例えば、場合によってはワークフローの内部処理にて、値の初期化等を実施するなど追加の処理を加える必要が生じる可能性が考えられます。

 

3. "アイテム作成時に自動起動する" のみにしない

上述の通りワークフローが失敗した場合には、該当のワークフローを手動で再起動する必要があります。上述 1. に記載した内容と比較すると細かな点となりますが、ワークフローがアイテム作成時に起動するだけの場合ですと、ワークフロー開始のトリガとなるイベントが起こせない (手動で開始、アイテムの更新では起動しない) ため、通常のオペレーションでは再起動はできません。


図 4 : ワークフロー開始オプション

ワークフローの再起動を運用上必要とするのであれば、アイテムが作成されたときにワークフローを自動的に開始するだけではなく、上下にあるいずれかのオプション (このワークフローを手動で開始できるようにする、アイテムが変更されたときにワークフローを自動的に開始する) を有効化する必要があります。

ただし、上図の状況においても、プログラム (SPWorkflowManager.StartWorkflow) から開始することは可能です。ワークフローの再起動のために別のインターフェースを用意する場合は、この設定値について考慮する必要はありません。

 

4. 開始フォームのパラメータを使用しない

プログラムなどで機械的にエラー終了したワークフローを管理者が再実行したり、プログラムによって自動再開 (SPWorkflowManager.StartWorkflow) する方法が考えられます。このような運用やシステム開発を前提とすると、ワークフローに開始フォームのパラメータがあると特別な考慮が必須となります。


図 5 : ワークフロー開始フォームのパラメータ

例えば、管理者がエラー終了したワークフローを手動で再実行する際においても、プログラムがワークフローを起動する際にも、開始時にユーザーが何を指定したかを確認する必要が出てきます。

  
図 6 : ワークフロー開始フォーム

管理者がこのような画面に遭遇した場合、何を入力して良いかを即時に判断できず、リカバリが遅れることにつながります。失敗したワークフローの再実行等を考慮するのであれば、このような混乱を防ぐため、開始フォームではなくアイテムの列などから情報を採取するよう作成することが現実的です。

 

ワークフローを監視・リトライする方法

このセクションでは、具体的にワークフローのエラーを監視したり、ワークフローを再起動するなど運用的な対処案についてご紹介します。

1.     リスト ビューから失敗したワークフローを抽出する方法

 ワークフローを作成すると、ワークフローの状態列がリストに登録されます。この列の値を使用して、エラー終了や開始時に失敗したワークフローを抽出して UI 上に表示することが可能です。

 1)   新しいビューを追加し、ビューの編集画面を開きます。
 2)   以下のように、ワークフローの状態列に対してフィルターを構成します。

赤枠で囲った値には、以下のテーブルを参考に対応する数値を指定します。

管理者は作成されたビューを使用して、エラー発生したワークフローを定期的に監視して、再起動するといった運用が可能となります。

 

2.     リストを監視し、エラー発生したアイテムを検出して再起動するプログラムを作成する

上記にて指定した管理者による監視および再起動を自動化することも可能です。以下のコードは実装のイメージを説明するためのサンプルであり、弊社として動作保障をしているものではありませんので、ご了承ください。

-- サンプル 開始 --
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using System.Threading;
using Microsoft.SharePoint.Workflow;

namespace SampleWorkflowRestart
{

class Program
{
    static void Main(string[] args)
    {
            try
            {
                string siteUrl = "https://sharepoint/sites/site1/web1/";  // サイトの URL を指定します。
                string listName = "ListName"; // リスト名を指定します。
                using (SPSite site = new SPSite(siteUrl))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        // リストに関連付けられたワークフローの再起動処理呼び出し
                       SPListRestartWorkflow(web.Lists[listName]);
                    }
                }

                Thread.Sleep(5000); // コンソール アプリケーションの場合のみ、ワークフロー起動後にすぐプロセスを終了させないため待機
                Console.WriteLine("完了しました。");
            }
            catch (Exception ex)
            {
               Console.WriteLine(ex.ToString());
            }
    }

    static public void SPListRestartWorkflow(SPList list)
    {
            foreach (SPField field in list.Fields)
            {
                // ワークフロー状態列を列挙します。
                if (field.Type == SPFieldType.WorkflowStatus)
                {
                    // 取得したワークフロー状態列が 3 の値のアイテムを取得します。
                    SPQuery query = new SPQuery();
                    query.Query = string.Format("<Where><Eq><FieldRef Name='{0}'/><Value Type='Int32'>{1}</Value></Eq></Where>", field.InternalName, 3);
                    SPListItemCollection items = list.GetItems(query);
                    // 列名に紐づく WorkflowAssociation を取得しておきます。
                    SPWorkflowAssociation assoc = list.WorkflowAssociations.GetAssociationByName(field.Title, System.Globalization.CultureInfo.CurrentCulture);

                    DebugWrite(string.Format("Restarting Workflow - Workflow : {0} ; List : {1}", assoc.Name, list.Title));

                     foreach (SPListItem item in items)
                    {
                        try
                        {
                            foreach (SPWorkflow wf in item.Workflows)
                            {
                                if (wf.AssociationId == assoc.Id)
                                {
                                    // ワークフローを一旦終了します。
                                    SPWorkflowManager.CancelWorkflow(wf);
                                    DebugWrite(string.Format("Cancel Workflow - Workflow : {0} ; List : {1} ; Item : {2}", assoc.Name, list.Title, item.Name));
                                    SPWorkflowManager spm = item.Web.Site.WorkflowManager;

                                    // ワークフローを開始します。
                                   spm.StartWorkflow(item, assoc, assoc.AssociationData, true);
                                   DebugWrite(string.Format("Start Workflow - Workflow : {0} ; List : {1} ; Item : {2}", assoc.Name, list.Title, item.Name));
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                           DebugWrite(string.Format("Start Workflow - Workflow : {0} ; List : {1} ; Item : {2}; Error : {3}", assoc.Name, list.Title, item.Name, ex.Message));
                        }
                   }
                   DebugWrite(string.Format("Restarted Workflow - Workflow : {0} ;List : {1}", assoc.Name, list.Title));
                }
            }
    }

    static public void DebugWrite(string dbgText)
    {
         Console.WriteLine(DateTime.Now.ToString() + "  " + dbgText);
    }
}
}

-- サンプル 終了 --

このサンプルは開始フォームのオプションに値が追加されているワークフローに対しては正常に起動させることができません。

この処理を実行するプロセス内で、別スレッドが生成されてワークフローが動作する形となります。メイン スレッドが終了するとプロセスが終了してしまうことを危惧し、Sleep メソッドの呼び出しは行っていますが、コンソール アプリケーション (または PowerShell スクリプトなど) で実装するよりは、常駐アプリケーションなどで実装することが望ましい処理内容となります。

以下のサイトなどを参考にしていただき、特にカスタム ジョブ定義などで実装することがお勧めです。

タイトル : [方法] すべての Web サーバーでコードを実行する
アドレス : https://msdn.microsoft.com/ja-jp/library/ff464297.aspx

今回の投稿は以上になります。