QueueSystem サービスを使用する
適用対象: Office 2010 | Project 2010 | Project Server 2010 | SharePoint Server 2010
Microsoft Project Server 2010 には、長時間実行される可能性のある非同期ジョブを処理するほか、多くの同時ジョブを管理するための Project Server Queue Service が含まれます。Project Server アプリケーションでは、多くの場合、Project Server キューの 1 つに送信されたジョブの完了時期が確認された後に、別のアクションの成功に依存するアクションが実行される必要があります。この記事では、Project Server Interface (PSI) の QueueSystem サービスを使用するユーティリティ クラスを開発し、そのユーティリティ クラスを使用して、Project Server キューのアクションをテストする方法について説明します。
この記事は、Microsoft Office Project Server 2007 用に記述された記事「[方法] QueueSystem Web サービスを使用する」の更新版です。Project Server キューでジョブの完了を確認する多くの方法があります。この記事の例は、テスト用サーバーを使ってキューの状態とジョブの状態を出力するものであり、これにより Project Server キューがどのように動作するかがわかります。Project 2010 SDK ダウンロード内の完全な UsingQueueSystem コード例には、WaitForQueue メソッドの追加のオーバーロードが含まれます。ダウンロードへのリンクについては、「Project デベロッパー ポータル」を参照してください。
この記事は次のセクションで構成されます。
GetJobCompletionState を使用して、キュージョブを待機する
ReadJobStatus を使用して、ジョブ グループを待機する
キュー ユーティリティ メソッドのテスト
テスト アプリケーションの実行
PSI には、QueueSubmitTimesheet、QueueArchiveCustomFields、QueueCreateProject など、Queue の名前で始まる多くのメソッドが含まれます。すべての非同期 PSI メソッドで、Project Server Queue Service が使用されます。また、すべての非同期 PSI メソッドには Queue で始まる名前が付けられています。PSI メソッドの名前が Queue で始まっていない場合、そのメソッドは同期的であり、そのメソッドでキュー サービスは使用されません。wsdl.exe で Web サービス プロキシが生成されるときに、Microsoft .NET Framework によって、非同期メソッドが自動的に追加されます。この自動生成されたプロキシ メソッドで、Project Server Queue Service は使用されません。
Project Web App (https://ServerName/ProjectServerName/_layouts/pwa/Admin/queue.aspx) の [キュー ジョブの管理] ページでは、QueueSystem メソッドを使用して、ジョブのフィルター処理と表示、および [ジョブ グリッド] の列の選択が行われます。Project Server では、2 つのキューが保持されますが、この 2 つのキューは、Microsoft Project Server Queue Service 2010 サービスによって管理されます。
タイムシート キューでは、Timesheet サービスで、キュー ベース メソッドからのジョブが管理されます。
プロジェクト キューでは、キュー ベースの他の Web メソッドからのジョブが管理 (作成、更新、保存、発行など) されます。
PSI の QueueSystem クラスには、ジョブの実行時間の見積もり、ジョブの状態の検索、およびジョブの取り消しを行うメソッドが含まれます。キュー ベースの PSI メソッドでは、ジョブ ID と共にメッセージが Project Server Queue Service に送信され、ジョブが処理されます。メソッドで 1 つのキュー ジョブのみが生成される場合、そのメソッドのパラメーターには jobUid が含まれます。これを使用して、キュー内のジョブの状態を検索できます。QueueArchiveProjects、QueueCleanUpTimesheets など、一部のメソッドでは、複数のキュー ジョブが生成されます。これらのメソッドでは、jobUid パラメーターは公開されません。
複数のキュー ジョブを生成する PSI メソッドを追跡するには、ReadMyJobStatus など、QueueStatusDataSet を返す任意のメソッドを使用できます。
相関ジョブは、互いに順次的に関連するキュー ジョブです。相関ジョブでは、GetJobCount、UnblockCorrelation、RetryCorrelation、GetProposedJobWaitTime、QueuePurgeArchivedJobs、CancelCorrelation など、メソッドで使用できるジョブ依存関係 GUID がキューによって作成されます。たとえば、QueueCreateProject メソッドおよび QueuePublish メソッドを使用して、プロジェクトの作成と発行を行うと、Project Server Queue Service によって、CorrelationGUID プロパティがプロジェクト GUID の値に設定されます。
注意
作成ジョブと発行ジョブの依存関係 GUID は同じです。また、Project Server によってこの 2 つのジョブは同じジョブ グループに内部的に追加されます。したがって、アプリケーションは、発行ジョブを開始する前に、キューをポーリングし、作成ジョブの終了を待機する必要はありません。Project Server Queue Service では、ジョブ グループ内で同じ依存関係 GUID を持つキュー ジョブの順序は自動的に処理されます。
相互に関連付けられていない複数のジョブを追跡するには、追跡 GUID を設定し、それを使用してキュー内でジョブ グループを追跡できます。複数のジョブに追跡 GUID を設定する方法の例については、「ReadJobStatus」を参照してください。追跡 GUID のその他の使用例については、「GetJobGroupWaitTime」および「GetJobGroupWaitTimeSimple」を参照してください。
この記事では、指定されたキュー ジョブが完了したタイミング、または指定されたキュー ジョブが、指定された時間を超過したタイミングを確認する、キュー ユーティリティのクラスとメソッドの作成方法について説明します。また、Project Server Queue Service でジョブがエラー状態になった場合について説明します。
重要
QueueDeleteProjects など、SDK トピック内にある、キュー ベース メソッドの多くのコード例には、キュー ジョブの状態を確認してから次にその状態を確認するまでの間で Thread.Sleep を使用する単純な WaitForQueue メソッドが含まれます。サーバーのスレッド プールでプロセスの実行を長期間ブロックしたり、多くの同時プロセス (タイムシートの送信など) の実行をブロックしたりすると、Project Server のパフォーマンスに重大な影響を与える可能性があります。Thread.Sleep メソッドは、サーバーで実行されるコンポーネントではなく、クライアントでの使用に限定することをお勧めします。
Project Server コンピューターで実行されるミドルウェア コンポーネントでは、指定された条件を満たせないループで Thread.Sleep を使用しないでください。代わりに、サーバー コンポーネントを使用して PSI キュー ベースのメソッドを呼び出すアプリケーションで、ジョブの状態を表示し、ユーザーがアプリケーションを更新してジョブの状態フィールドを更新できるようにする必要があります。この記事のコード例にある WaitForQueue メソッドでは、CurrentThread.Join メソッドが使用されます。このメソッドは、指定された期間 (ミリ秒) またはスレッドが終了するまで呼び出し元のスレッドをブロックしますが、他のスレッド プロセスの続行は許可します。
サンプルの QueueUtilities クラスには、最大待機秒数を指定できるように TimeoutSec プロパティが含まれています。サンプル コードではジョブの状態が返されます。また、エラー状態が返される可能性もあります。Project Server キューの動作をわかりやすく説明するために、WaitForQueue メソッドでは、ジョブが完了するまでの秒数に関する Project Server Queue Service の初期見積もりも返されます。
GetJobCompletionState を使用して、キュージョブを待機する
手順 1. で、指定されたキュー ジョブを待機する WaitForQueue メソッドの例を使用して、キュー ユーティリティのクラスを作成する方法を示します。この WaitForQueue メソッドでは、QueueSystem サービスの GetJobCompletionState メソッドと ReadJobStatusSimple メソッドが使用されます。
手順 1. キュー ジョブを待機するユーティリティ メソッドを作成するには
Microsoft Visual Studio 2010 で、UsingQueueSystem という名前のコンソール アプリケーションを作成します。次のように、Project サービスと QueueSystem サービスのエンドポイント アドレスを定義する app.config という名前のアプリケーション構成ファイルを追加します。
<configuration> <system.serviceModel> <behaviors> <endpointBehaviors> <behavior name="basicHttpBehavior"> <clientCredentials> <windows allowedImpersonationLevel="Impersonation" /> </clientCredentials> </behavior> </endpointBehaviors> </behaviors> <bindings> <basicHttpBinding> <binding name="basicHttpConf" sendTimeout="01:00:00" maxBufferPoolSize="500000000" maxReceivedMessageSize="500000000"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="500000000" /> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Ntlm" realm="http" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc" behaviorConfiguration="basicHttpBehavior" binding="basicHttpBinding" bindingConfiguration="basicHttpConf" contract="SvcProject.Project" name="basicHttp_Project" /> <endpoint address="https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc" behaviorConfiguration="basicHttpBehavior" binding="basicHttpBinding" bindingConfiguration="basicHttpConf" contract="SvcQueueSystem.QueueSystem" name="basicHttp_QueueSystem" /> </client> </system.serviceModel> </configuration>
Project Server インストール用のサーバー名および Project Web App インスタンス名を変更します。
アプリケーションに以下の参照を追加します。
Microsoft.Office.Project.Server.Library
WCF ベースのアプリケーションの ProjectServerServices プロキシ アセンブリ。
PSI サービスのプロキシ アセンブリを作成して使用する方法については、「WCF ベースのコード サンプルの前提条件」を参照してください。
キュー ユーティリティを含めるアプリケーションにクラスを追加します。たとえば、クラスに QueueUtilities という名前を付けます。クラス コンストラクター、定数、変数、およびクラスの各プロパティを次のように追加します。
INCREMENTAL_SLEEPTIME 定数には、通常の短いキュー ジョブの完了に要するミリ秒数を指定する必要があります。
queueSystemClient には、PSI の WCF インターフェイス内の QueueSystemClient オブジェクトが含まれます。
logWriter には、ログ ファイルを書き込むための StreamWriter オブジェクトが含まれます。
queueMessage 変数を使用して、ログ ファイルのメッセージを保持します。
TimeoutSec プロパティは、キューを待機する最大秒数です。
WriteLogFile プロパティでは、キュー操作のログ ファイルを書き込むかどうかを指定します。
2007 以来、TotalUnprocessedJobCount プロパティでは、ReadAllJobStatusSimple メソッドを使用して、すべてのキュー ジョブと共に QueueStatusDataSet が取得されます。未処理のジョブの数は、Status テーブルの中で、JobCompletionState が ReadyForProcessing になっている行の数です。
QueueUtilities コンストラクターでは、queueSystemClient、logWriter、TimeoutSec、および WriteLogFile が初期化されます。
以下のコードでは、QueueUtilities クラスのコンストラクターとプロパティが実装されます。
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using SvcQueueSystem; using PSLib = Microsoft.Office.Project.Server.Library; namespace Microsoft.SDK.Project.Samples.Utilities { public class QueueUtilities { // Incremental sleep time is 2 seconds. private const int INCREMENTAL_SLEEPTIME = 2000; private static QueueSystemClient queueSystemClient; private static StreamWriter logWriter; private string queueMessage = string.Empty; public QueueUtilities(QueueSystemClient qsClient, int timeout, StreamWriter logFile) { queueSystemClient = qsClient; logWriter = logFile; // The default timeout is 2 minutes. if (timeout < 0) this.TimeoutSec = 2 * 60; else this.TimeoutSec = timeout; if (logFile == null) this.WriteLogFile = false; else this.WriteLogFile = true; } #region Class Properties /// <summary>Number of seconds to wait for the queue.</summary> /// <value>Timeout for WaitForQueue and WaitForQueueByGroup calls.</value> public int TimeoutSec { get; set; } /// <summary>Specifies whether to write a log file for queue operations.</summary> /// <value>True: write a log file. False: do not write a log file.</value> public bool WriteLogFile { get; set; } /// <summary>Gets the total number of unprocessed jobs in the queue.</summary> /// <value>The number of unprocessed jobs is the number of rows in the Status /// table where the JobCompletionState is ReadyForProcessing.</value> public int TotalUnprocessedJobCount { get { DateTime startTime = new DateTime(2007, 1, 1); DateTime endTime = DateTime.Now; int maxRows = Int32.MaxValue; QueueStatusDataSet queueStatusDs = queueSystemClient.ReadAllJobStatusSimple( startTime, endTime, maxRows, false, SortColumn.Undefined, SortOrder.Undefined); if (queueStatusDs == null) { String errorMessage = "ReadAllJobStatusSimple returned a null QueueStatusDataSet"; Exception ex = new Exception(errorMessage); throw ex; } else { return queueStatusDs.Status.Select("JobCompletionState = " + (int)JobState.ReadyForProcessing).Length; } } } #endregion /* Add WaitForQueue method overloads. */ /* Add WaitForQueueByGroup method (Procedure 2). */ } }
WaitForQueue メソッドで使用するメソッドを追加します。キューでジョブの処理が完了したことが、現在のジョブの状態に示されている場合、IsJobStateProcessed は true を返します。テスト サンプルでは、テスト情報の出力に LogQueueStatus、LogJobStatus、および LogQueueTimeout が使用されます。このテスト出力方法は、運用アプリケーションで使用されない可能性があります。
/// <summary>Indicates whether the queue has completed processing the job.</summary> /// <param name="currentState">State of the queue job.</param> /// <returns>True if the job state is processed; false if the job state is not processed.</returns> private static bool IsJobStateProcessed(JobState currentState) { bool jobProcessed = false; if ((currentState == JobState.Failed) || (currentState == JobState.FailedNotBlocking) || (currentState == JobState.CorrelationBlocked) || (currentState == JobState.Canceled) || (currentState == JobState.Success)) { jobProcessed = true; } return jobProcessed; } /// <summary>Log the status of the queue.</summary> public void LogQueueStatus() { queueMessage = "\r\nNumber of unprocessed jobs in the queue: " + TotalUnprocessedJobCount.ToString(); if (WriteLogFile) logWriter.WriteLine(queueMessage); Console.WriteLine(queueMessage); } /// <summary>Log the status of a job in the queue.</summary> private void LogJobStatus(QueueStatusDataSet.StatusRow jobRow) { string jobStatus = "\r\nQueue job: " + GetStatusFields(); Console.WriteLine(jobStatus, (JobState)jobRow.JobCompletionState, jobRow.JobGUID, jobRow.JobGroupGUID, jobRow.CorrelationGUID, (QueueMsgType)jobRow.MessageType, jobRow.IsMachineNameNull() ? "DbNull" : jobRow.MachineName, jobRow.PercentComplete, jobRow.QueueEntryTime, jobRow.HasErrors); } /// <summary>Log the queue timeout.</summary> /// <param name="message"></param> private void LogQueueTimeout(QueueStatusDataSet.StatusDataTable jobs, string message) { string jobStatus = "\r\nQueue timeout: " + GetStatusFields(); if (jobs != null && jobs.Count > 0) { foreach (QueueStatusDataSet.StatusRow job in jobs) { Console.WriteLine(jobStatus, (JobState)job.JobCompletionState, job.JobGUID, job.JobGroupGUID, job.CorrelationGUID, (QueueMsgType)job.MessageType, job.IsMachineNameNull() ? "DbNull" : job.MachineName, job.PercentComplete, job.QueueEntryTime, job.HasErrors); } } queueMessage = string.Format("{0}: time exceeeded {1} seconds!", message, (TimeoutSec).ToString()); if (WriteLogFile) { logWriter.WriteLine(queueMessage); logWriter.Flush(); } Console.WriteLine(queueMessage); } // Fields for the status line in LogJobStatus and QueueTimeout. private string GetStatusFields() { string statusFields = "JobCompletionState = {0}, \r\n\tJobGuid = {1}, \r\n\tJobGroupGuid = {2}, "; statusFields += "\r\n\tCorrelationGUID = {3}, \r\n\tMessageType = {4}, MachineName = {5}, "; statusFields += "\r\n\tPercentComplete = {6}, QueueEntryTime = {7}, \r\n\tHasErrors = {8}"; return statusFields; }
WaitForQueue メソッドを QueueUtilities クラスに追加します。このクラスは、JobState 列挙定数を返します。この例の WaitForQueue メソッドは、GetJobCompletionState メソッドと while (true) ループを使用して、キュー ジョブの状態を確認します。初回のループで、GetJobWaitTime メソッドは、キューがジョブを処理するまでの推定待機時間を示します。キュー サービスでジョブの処理が終了した場合や、ジョブの状態が不明な場合、またはプロセスが最大待機時間を超過した場合、WaitForQueue はループを終了してジョブの状態を返します。
ジョブの状態は、JobState 定数の 1 つである可能性があります。ジョブの状態が、Success、Failed など、IsJobStateProcessed メソッドに指定された状態の 1 つである場合、キューはジョブの処理を終了しています。この例では、WaitForQueue は、ReadJobStatusSimple メソッドを使用して、ジョブの状態を QueueStatusDataSet に取得します。この場合、ジョブの状態は、テスト アプリケーションでの出力のみを目的として使用されます。
以下のコードでは、WaitForQueue に 2 つのオーバーロードがあります。
最初のオーバーロードのパラメーターは、jobUid と errorString です。メソッドは、GetJobCompletionState への呼び出しからのすべてのエラーを出力します。
WaitForQueue メソッドの 2 番目のオーバーロードには、jobUid パラメーターのみが含まれます。メソッドは、最初のオーバーロードを呼び出し、エラー出力を破棄します。
注意
Project 2010 SDK ダウンロード内の完全なソリューションには、WaitForQueue メソッドの追加のオーバーロードがいくつか含まれます。これらのオーバーロードは、指定されたメッセージの種類とジョブの状態を待機します。
#region WaitForQueue Method Overloads /// <summary>Waits for a specified queue job to complete.</summary> /// <param name="jobUid">The job GUID.</param> /// <param name="errorString">The error string returned by the queue.</param> public JobState WaitForQueue(Guid jobUid, out String errorString) { int timeSlept = 0; bool firstPass = true; // First iteration through the while statement. // Increase the incremental sleep time, if the timeout exceeds 60 seconds. int sleepInterval = (TimeoutSec * 1000 / 60 > INCREMENTAL_SLEEPTIME) ? this.TimeoutSec / 60 : INCREMENTAL_SLEEPTIME; queueMessage = string.Format("\r\n**WaitForQueue, jobUid = {0}", jobUid.ToString()); Console.WriteLine(queueMessage); if (WriteLogFile) logWriter.WriteLine(queueMessage); LogQueueStatus(); while (true) { if (firstPass) { // Get the estimated time to wait for the queue to process the job. // The output from GetJobWaitTime is in seconds. int estWaitTime = queueSystemClient.GetJobWaitTime(jobUid); queueMessage = string.Format("Estimated job wait time: {0} seconds", estWaitTime); Console.WriteLine(queueMessage); if (WriteLogFile) logWriter.WriteLine(queueMessage); firstPass = false; } JobState jobState = queueSystemClient.GetJobCompletionState(out errorString, jobUid); QueueStatusDataSet jobStatus = queueSystemClient.ReadJobStatusSimple(new Guid[] { jobUid }, true); if (WriteLogFile) { LogJobStatus(jobStatus.Status[0]); logWriter.WriteLine("\r\n\tjobState: {0}\r\n\tJobCompletionState error: '{1}" + "\r\n\r\n--- QueueStatusDataSet from ReadJobStatusSimple: ---\r\n", jobState.ToString(), errorString); jobStatus.WriteXml(logWriter); logWriter.WriteLine(); } if (jobState == JobState.Unknown) { if (WriteLogFile) { queueMessage = "Job status is unknown; was the job placed on the queue?" + "\r\n\t-- returning from WaitForQueue"; logWriter.WriteLine(queueMessage); Console.WriteLine(queueMessage); } return jobState; } if (IsJobStateProcessed(jobState)) { if (WriteLogFile) { queueMessage = "Job completed, returning from WaitForQueue"; logWriter.WriteLine(queueMessage); Console.WriteLine(queueMessage); } return jobState; } Thread.CurrentThread.Join(sleepInterval); timeSlept += sleepInterval; if (timeSlept > TimeoutSec * 1000) { if (WriteLogFile) LogQueueTimeout(jobStatus.Status, "WaitForQueue"); return JobState.Unknown; } } } /// <summary>Waits for a specific queue job to complete.</summary> /// <param name="jobUid">The job GUID.</param> public JobState WaitForQueue(Guid jobUid) { String errorString = String.Empty; return WaitForQueue(jobUid, out errorString); } #endregion WaitForQueue Method Overloads
キュー ユーティリティ メソッドのテスト には、両方の WaitForQueue オーバーロードを使用するコード例が含まれます。テスト アプリケーションの実行 でサンプル出力を示します。
ReadJobStatus を使用して、ジョブ グループを待機する
サンプルの WaitForQueueByGroup メソッドは、ReadJobStatus メソッドのパラメーターとして、QueueStatusRequestDataSet を使用します。このパラメーターには、QueueStatusDataSet が返されます。
手順 2. ジョブ グループを待機するには
QueueStatusRequestDataSet を作成するためのコード、および while(true) ループで使用する他の変数を使用して、WaitForQueueByGroup メソッドを追加します。jobUids リスト内のジョブごとに、StatusRequest テーブルに行を追加します。
注意
テスト データ出力用の if (WriteLogFile) ブロックを含めない場合、WaitForQueueByGroup を簡素化できます。
/// <summary>Waits for a specified queue job group to complete.</summary> /// <param name="jobUids">List of the job GUIDs.</param> /// <param name="jobGroupUid">The job group GUID.</param> /// <returns>True if all jobs are successful; false if any job is unsuccessful.</returns> public bool WaitForQueueByGroup(List<Guid> jobUids, Guid jobGroupUid) { QueueStatusRequestDataSet queueStatusRequestDs = new QueueStatusRequestDataSet(); foreach (Guid jobUid in jobUids) { QueueStatusRequestDataSet.StatusRequestRow srRow = queueStatusRequestDs.StatusRequest.NewStatusRequestRow(); srRow.JobGUID = jobUid; srRow.JobGroupGUID = jobGroupUid; if (WriteLogFile) { queueMessage = string.Format("\r\n**WaitForQueueByGroup: jobUid: {0}, jobGroupUid: {1}\r\n", jobUid.ToString(), jobGroupUid.ToString()); logWriter.Write(queueMessage); Console.Write(queueMessage); } queueStatusRequestDs.StatusRequest.AddStatusRequestRow(srRow); } LogQueueStatus(); QueueStatusDataSet statusDataSet = null; int timeSlept = 0; // Increase the incremental sleep time, if the timeout exceeds 60 seconds. int sleepInterval = (TimeoutSec * 1000 / 60 > INCREMENTAL_SLEEPTIME) ? TimeoutSec / 60 : INCREMENTAL_SLEEPTIME; if (WriteLogFile) { logWriter.WriteLine("\r\nSleep interval: {0}\r\n\r\n--- QueueStatusRequestDataSet: ---\r\n", sleepInterval.ToString()); queueStatusRequestDs.WriteXml(logWriter); logWriter.WriteLine("\r\n"); Console.WriteLine("Sleep interval: {0}", sleepInterval.ToString()); } bool isGroupSuccessful; bool isGroupCompleted; /* Add the while(true) loop here */ }
while(true) ループを追加します。ループでは、ReadJobStatus を呼び出して、QueueStatusRequestDataSet 内のジョブごとに QueueStatusDataSet.Status テーブルの 1 行を取得します。
while (true) ループでは、QueueStatusDataSet.StatusRow テーブル内のジョブ行ごとにジョブの状態を確認し、失敗したジョブの状態との比較に従って、isGroupSuccessful 値を設定します。キューでジョブの処理が完了すると、WaitForQueueByGroup は、ループを終了し、isGroupSuccessful 値を返します。
while (true) { //String errorString = String.Empty; statusDataSet = queueSystemClient.ReadJobStatus(queueStatusRequestDs, false, SortColumn.Undefined, SortOrder.Undefined); if (WriteLogFile && statusDataSet.Status.Count > 0) { logWriter.WriteLine("\r\n\r\n--- QueueStatusDataSet from ReadJobStatus: ---\r\n"); statusDataSet.WriteXml(logWriter); logWriter.WriteLine(); } if (statusDataSet.Status.Rows.Count != 0) { isGroupSuccessful = true; isGroupCompleted = true; foreach (System.Data.DataRow jobRow in statusDataSet.Status.Rows) { QueueStatusDataSet.StatusRow singleJobRow = (QueueStatusDataSet.StatusRow)jobRow; JobState jobState = (JobState)singleJobRow.JobCompletionState; if (WriteLogFile) LogJobStatus(singleJobRow); isGroupSuccessful = isGroupSuccessful & (jobState != JobState.Failed) & (jobState != JobState.FailedNotBlocking) & (jobState != JobState.Canceled); if (!IsJobStateProcessed(jobState)) { if (WriteLogFile) { logWriter.WriteLine("\t...job not completed yet."); } isGroupCompleted = false; break; } } if (isGroupCompleted) { // The queue is finished processing the jobs. if (WriteLogFile) { queueMessage = "\nJob completed, returning from WaitForQueueByGroup"; logWriter.WriteLine(queueMessage); Console.WriteLine(queueMessage); } return isGroupSuccessful; } } else // statusDataSet.Status.Rows.Count == 0 { if (WriteLogFile) { queueMessage = "ReadJobStatus returned an empty QueueStatusDataSet"; logWriter.WriteLine(queueMessage); Console.WriteLine(queueMessage); } return true; } Thread.CurrentThread.Join(sleepInterval); timeSlept += sleepInterval; if (timeSlept > TimeoutSec * 1000) { if (WriteLogFile) LogQueueTimeout(statusDataSet.Status, "WaitForQueueByGroup"); return false; } }
キュー ユーティリティ メソッドのテスト には、2 つのジョブについて、WaitForQueueByGroup メソッドを使用するコード例が含まれます。テスト アプリケーションの実行 でサンプル出力を示します。
キュー ユーティリティ メソッドのテスト
UsingQueueSystem は、QueueUtilities オブジェクトをインスタンス化し、2 つの WaitForQueue メソッド オーバーロードと WaitForQueueByGroup メソッドをテストするコンソール アプリケーションです。テスト アプリケーションでは、2 つのタスクを持つプロジェクトの作成、保存、および発行を行った後、プロジェクトのチェックアウト、タスクの追加、プロジェクトのチェックインを行います。
UsingQueueSystem アプリケーションのオプションの引数を使用して、キュー ジョブを待機せずにテストを実行したり、キュー ジョブの最大待機時間を指定したり、QueueStatusRequestDataSet オブジェクトと QueueStatusDataSet オブジェクトの内容が含まれるログ ファイルを書き込んだり、プロジェクト名および追加したタスク名を指定したりできます。
手順 3. WaitForQueue メソッドのテスト アプリケーションを作成するには
UsingQueueSystem ソリューションの Program.cs ファイルで、次のように、using 宣言、クラスの定数、ParseCommandLine メソッド、ConfigClientEndpoints メソッド、および WriteFaultOutput メソッドを追加し、FaultException データを出力するための Helpers クラスを追加します。
using System; using System.IO; using System.Threading; using System.Collections.Generic; using System.Diagnostics; using System.ServiceModel; using System.Xml; using PSLibrary = Microsoft.Office.Project.Server.Library; using Microsoft.SDK.Project.Samples.Utilities; namespace Microsoft.SDK.Project.Samples.UsingQueueSystem { class Program { private const string ENDPOINT_PROJECT = "basicHttp_Project"; private const string ENDPOINT_QUEUESYSTEM = "basicHttp_QueueSystem"; private const string SESSION_DESC = "Sample add task to project"; // Queue session description. private const string PROJ_NAME = "Add2ProjTest_"; // Default project name prefix. private static SvcProject.ProjectClient projectClient; private static SvcQueueSystem.QueueSystemClient queueSystemClient; private static int maxSeconds2Wait = 20; // Maximum number of seconds to wait for the queue. private static bool wait4Queue = true; // Wait for the queue to finish, before proceeding. private static bool writeQueueLog = false; // If true, write a log file and queue datasets to XML files. // If false, send output only to the console. private static string projectName = PROJ_NAME; // If project name is not specified, add a GUID to the name. private static string addedTaskName = "Added task"; private static QueueUtilities queueUtilities = null; // Change the output directory for your computer. private const string OUTPUT_FILES = @"C:\Project\Samples\Output\"; private static string logFileQueueSystem; static void Main(string[] args) { string comment = string.Empty; // Comment for output to the console and log file. Guid projUid = Guid.Empty; // GUID of the project. string projName = string.Empty; // Name of the project. Guid jobUid = Guid.Empty; // GUID of the queue job. Guid queueSessionUid = Guid.NewGuid(); // Guid of the queue session, to track multiple jobs. StreamWriter logWriter = null; if (!ParseCommandLine(args)) { Usage(); ExitApp(); } logFileQueueSystem = OUTPUT_FILES + "QueueOutput.log"; /* Add the try - catch - finally blocks here. */ } /* Add the CreateSampleProject method and the AddTask method here. */ // Use the endpoints that are defined in app.config to configure the client. public static void ConfigClientEndpoints(string endpt) { if (endpt == ENDPOINT_PROJECT) projectClient = new SvcProject.ProjectClient(endpt); else if (endpt == ENDPOINT_QUEUESYSTEM) queueSystemClient = new SvcQueueSystem.QueueSystemClient(endpt); } // Extract a PSClientError object from the WCF FaultException object, and // then display the exception details and each error in the PSClientError stack. private static void WriteFaultOutput(FaultException fault) { string errAttributeName; string errAttribute; string errOut; string errMess = "".PadRight(30, '=') + "\r\n" + "Error details: \n" + "\r\n"; PSLibrary.PSClientError error = Helpers.GetPSClientError(fault, out errOut); errMess += errOut; PSLibrary.PSErrorInfo[] errors = error.GetAllErrors(); PSLibrary.PSErrorInfo thisError; for (int i = 0; i < errors.Length; i++) { thisError = errors[i]; errMess += "\r\n".PadRight(30, '=') + "\r\nPSClientError output:\r\n\n"; errMess += thisError.ErrId.ToString() + "\n"; for (int j = 0; j < thisError.ErrorAttributes.Length; j++) { errAttributeName = thisError.ErrorAttributeNames()[j]; errAttribute = thisError.ErrorAttributes[j]; errMess += "\r\n\t" + errAttributeName + ": " + errAttribute; } } Console.WriteLine(errMess); } // Parse the command line. Return true if there are no errors. private static bool ParseCommandLine(string[] args) { int i; bool error = false; int argsLength = args.Length; for (i = 0; i < argsLength; i++) { if (error) break; if (args[i].StartsWith("-") || args[i].StartsWith("/")) args[i] = "*" + args[i].Substring(1).ToLower(); switch (args[i]) { case "*noq": case "*noqueue": wait4Queue = false; break; // Limit timeout seconds from -1 to 1800 (30 minutes). // If timeout = -1, use the default 2 minutes in QueueUtilities. case "*timeout": if (++i >= argsLength) return false; int seconds = Convert.ToInt32(args[i]); if (seconds < -1 || seconds > 1800) return false; maxSeconds2Wait = seconds; break; case "*writelog": writeQueueLog = true; break; case "*projname": if (++i >= argsLength) return false; projectName = args[i]; break; case "*taskname": if (++i >= argsLength) return false; addedTaskName = args[i]; break; case "*?": default: error = true; break; } } return !error; } private static void Usage() { string example = "Usage: UsingQueueSystem [-noQueue | -noQ] [-timeout seconds] [-writeLog] "; example += "\n\t\t\t[-projName \"Project Name\"] [-taskName \"Added task\"]"; Console.WriteLine(example); Console.WriteLine(" -noQueue: Do not wait for the queue to finish. Default: wait."); Console.WriteLine(" -timeout seconds: Number of seconds to wait for the queue,"); Console.WriteLine("\t\t where seconds can be -1 to 1800 (30 minutes)."); Console.WriteLine(" -writeLog: Write a log file for the queue. Default: send output to console."); Console.WriteLine(" -projName: Name of the project to be created. Default: Add2ProjTest_GUID."); Console.WriteLine(" -taskName: Name of the task to add, after the sample project is created."); } private static void ExitApp() { Console.Write("\nPress any key to exit... "); Console.ReadKey(true); Environment.Exit(0); } } class Helpers { // Helper method: GetPSClientError. /// <summary> /// Extract a PSClientError object from the ServiceModel.FaultException, /// for use in output of the GetPSClientError stack of errors. /// </summary> /// <param name="e"></param> /// <param name="errOut">Shows that FaultException has more information /// about the errors than PSClientError has. FaultException can also contain /// other types of errors, such as failure to connect to the server.</param> /// <returns>PSClientError object, for enumerating errors.</returns> public static PSLibrary.PSClientError GetPSClientError(FaultException e, out string errOut) { const string PREFIX = "GetPSClientError() returns null: "; errOut = string.Empty; PSLibrary.PSClientError psClientError = null; if (e == null) { errOut = PREFIX + "Null parameter (FaultException e) passed in."; psClientError = null; } else { // Get a ServiceModel.MessageFault object. var messageFault = e.CreateMessageFault(); if (messageFault.HasDetail) { using (var xmlReader = messageFault.GetReaderAtDetailContents()) { var xml = new XmlDocument(); xml.Load(xmlReader); var serverExecutionFault = xml["ServerExecutionFault"]; if (serverExecutionFault != null) { var exceptionDetails = serverExecutionFault["ExceptionDetails"]; if (exceptionDetails != null) { try { errOut = exceptionDetails.InnerXml + "\r\n"; psClientError = new PSLibrary.PSClientError(exceptionDetails.InnerXml); } catch (InvalidOperationException ex) { errOut = PREFIX + "Unable to convert fault exception info "; errOut += "a valid Project Server error message. Message: \n\t"; errOut += ex.Message; psClientError = null; } } else { errOut = PREFIX + "The FaultException e is a ServerExecutionFault, " + "but does not have ExceptionDetails."; } } else { errOut = PREFIX + "The FaultException e is not a ServerExecutionFault."; } } } else // There is no detail in the MessageFault. { errOut = PREFIX + "The FaultException e does not have any detail."; } } errOut += "\r\n" + e.ToString() + "\r\n"; return psClientError; } } }
CreateSampleProject メソッドと AddTask メソッドを追加します。
static private Guid CreateSampleProject(out string projName, out bool projCreated) { SvcProject.ProjectDataSet projectDs = new SvcProject.ProjectDataSet(); Guid jobUid; Guid projectUid = Guid.NewGuid(); // Create the project. SvcProject.ProjectDataSet.ProjectRow projectRow = projectDs.Project.NewProjectRow(); projectRow.PROJ_UID = projectUid; projName = (projectName == PROJ_NAME) ? PROJ_NAME + projectUid.ToString() : projectName; projectRow.PROJ_NAME = projName; projectRow.PROJ_TYPE = (int)PSLibrary.Project.ProjectType.Project; projectDs.Project.AddProjectRow(projectRow); // Add some tasks. SvcProject.ProjectDataSet.TaskRow taskOne = projectDs.Task.NewTaskRow(); taskOne.PROJ_UID = projectRow.PROJ_UID; taskOne.TASK_UID = Guid.NewGuid(); // The task duration format must be specified. taskOne.TASK_DUR_FMT = (int)PSLibrary.Task.DurationFormat.Day; taskOne.TASK_DUR = 4800; // 8 hours in duration units (minute/10) taskOne.TASK_NAME = "Task One"; taskOne.TASK_IS_MANUAL = false; projectDs.Task.AddTaskRow(taskOne); SvcProject.ProjectDataSet.TaskRow taskTwo = projectDs.Task.NewTaskRow(); taskTwo.PROJ_UID = projectRow.PROJ_UID; taskTwo.TASK_UID = Guid.NewGuid(); // The task duration format must be specified. taskTwo.TASK_DUR_FMT = (int)PSLibrary.Task.DurationFormat.Day; taskTwo.TASK_DUR = 4800; // 8 hours in duration units (minute/10) taskTwo.TASK_NAME = "Task Two"; taskTwo.TASK_IS_MANUAL = false; projectDs.Task.AddTaskRow(taskTwo); // Make task two dependent on task one. SvcProject.ProjectDataSet.DependencyRow dependentRow = projectDs.Dependency.NewDependencyRow(); dependentRow.LINK_UID = Guid.NewGuid(); dependentRow.PROJ_UID = projectUid; dependentRow.LINK_PRED_UID = taskOne.TASK_UID; dependentRow.LINK_SUCC_UID = taskTwo.TASK_UID; projectDs.Dependency.AddDependencyRow(dependentRow); // Create the project. jobUid = Guid.NewGuid(); projectClient.QueueCreateProject(jobUid, projectDs, false); projCreated = true; if (wait4Queue) { Console.ForegroundColor = ConsoleColor.Cyan; SvcQueueSystem.JobState qState = queueUtilities.WaitForQueue(jobUid); Console.ResetColor(); Console.WriteLine("\nResult of WaitForQueue, after QueueCreateProject: {0} ({0:D})", qState); if (qState == SvcQueueSystem.JobState.Unknown) projCreated = false; } return projectRow.PROJ_UID; } // Create a ProjectDataSet that has one new task. private static SvcProject.ProjectDataSet AddTask(Guid projUid, string newTaskName) { SvcProject.ProjectDataSet newProjData = new SvcProject.ProjectDataSet(); SvcProject.ProjectDataSet.TaskRow newTask = newProjData.Task.NewTaskRow(); newTask.PROJ_UID = projUid; newTask.TASK_UID = Guid.NewGuid(); newTask.TASK_NAME = newTaskName; newTask.TASK_DUR = 9600; newTask.TASK_DUR_FMT = (int)PSLibrary.Task.DurationFormat.Day; newTask.TASK_IS_MANUAL = false; newProjData.Task.AddTaskRow(newTask); return newProjData; }
try、catch、finally の各ブロックを追加します。これらのブロックで、クライアント エンドポイントの構成、QueueUtilities オブジェクトのインスタンス化、サンプル プロジェクトの作成と発行を行った後に、プロジェクトのチェックアウトとタスクの追加を行います。
try { if (writeQueueLog) { if (!Directory.Exists(OUTPUT_FILES)) Directory.CreateDirectory(OUTPUT_FILES); FileInfo logFile = new FileInfo(logFileQueueSystem); logWriter = new StreamWriter(logFileQueueSystem, false); } ConfigClientEndpoints(ENDPOINT_PROJECT); ConfigClientEndpoints(ENDPOINT_QUEUESYSTEM); queueUtilities = new QueueUtilities(queueSystemClient, maxSeconds2Wait, logWriter); queueUtilities.TimeoutSec = maxSeconds2Wait; // Create and publish a sample project. comment = string.Format("Creating a project. Queue wait time: {0}\n", (wait4Queue) ? maxSeconds2Wait.ToString() + " seconds" : "WaitForQueue not called"); Console.WriteLine(comment); if (writeQueueLog) logWriter.WriteLine(comment); bool projCreated; projUid = CreateSampleProject(out projName, out projCreated); jobUid = projUid; // Set the queue job GUID to the project GUID. if (projCreated) { comment = string.Format("\r\nCreated a project, GUID = {0}", projUid.ToString()); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(comment); Console.ResetColor(); if (writeQueueLog) logWriter.WriteLine(comment); } projectClient.QueuePublish(jobUid, projUid, true, string.Empty); string errorString = string.Empty; if (wait4Queue) { Console.ForegroundColor = ConsoleColor.Cyan; SvcQueueSystem.JobState qState = queueUtilities.WaitForQueue(jobUid, out errorString); Console.ResetColor(); comment = string.Format("\r\nWait for QueuePublish, errorString: '{0}'", errorString); Console.WriteLine(comment); Console.WriteLine("\nResult of WaitForQueue, after QueuePublish: {0} ({0:D})", qState); if (writeQueueLog) logWriter.WriteLine(comment); } // Check out the project. comment = string.Format("\r\nChecking out the project: {0}", projName); Console.WriteLine(comment); if (writeQueueLog) logWriter.WriteLine(comment); projectClient.CheckOutProject(projUid, queueSessionUid, SESSION_DESC); // Create a task to add. SvcProject.ProjectDataSet newProjData = AddTask(projUid, addedTaskName); // Add the task to the project by using the current queue session. List<Guid> jobsInGroup = new List<Guid>(); jobUid = Guid.NewGuid(); jobsInGroup.Add(jobUid); projectClient.QueueAddToProject(jobUid, queueSessionUid, newProjData, false); comment = string.Format("Added a task, name: '{0}'", addedTaskName); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(comment); Console.ResetColor(); if (writeQueueLog) logWriter.WriteLine(comment); // Check in the project. comment = "Checking in the project.\n"; Console.WriteLine(comment); if (writeQueueLog) logWriter.WriteLine(comment); jobUid = Guid.NewGuid(); jobsInGroup.Add(jobUid); projectClient.QueueCheckInProject(jobUid, projUid, false, queueSessionUid, SESSION_DESC); if (wait4Queue) { Console.ForegroundColor = ConsoleColor.Cyan; bool qResult = queueUtilities.WaitForQueueByGroup(jobsInGroup, queueSessionUid); Console.ResetColor(); comment = string.Format("Result of WaitForQueueByGroup, after QueueCheckInProject: {0}", qResult); Console.WriteLine(comment); if (writeQueueLog) logWriter.WriteLine(comment); } } // Use the WCF FaultException, because the ASMX SoapException does not // exist in a WCF-based application. catch (FaultException fault) { Console.ForegroundColor = ConsoleColor.Red; WriteFaultOutput(fault); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(ex.Message); } finally { Console.ResetColor(); ExitApp(); }
テスト アプリケーションでキューを待機する場合、CreateSampleProject メソッドは、QueueCreateProject メソッドを呼び出し、キュー ジョブの新しい GUID を使用して WaitForQueue オーバーロードを呼び出します。キュー ジョブが終了すると、アプリケーションは、QueuePublish を呼び出し、エラー文字列を出力する WaitForQueue オーバーロードを呼び出します。
発行ジョブが完了すると、アプリケーションは、キュー セッションの新しい GUID を使用し、CheckOutProject を呼び出して複数のジョブを追跡し、AddTask を呼び出して、新しいタスクが含まれる ProjectDataSet を作成し、QueueAddToProject を呼び出します。この時点でキュー ジョブを待機する代わりに、アプリケーションは、jobsInGroup リストにジョブの GUID を追加します。最後に、アプリケーションは、QueueCheckInProject を呼び出し、jobsInGroup リストに新しいキュー ジョブを追加して、ジョブのグループとキュー セッションの GUID を使用して WaitForQueueByGroup を呼び出します。
テスト アプリケーションの実行
UsingQueueSystem テスト アプリケーションの目的は、キューを待機する必要がある場合とキューを待機する必要がない場合の例を示すこと、および一部の QueueSystem メソッドによって使用されるデータと返されるデータを確認することです。
QueueUtilities クラスは、テスト目的のサンプルです。GetJobCompletionState メソッドは、エラー情報を XML 形式で errorString パラメーターに返します。エラーがない場合、xmlError パラメーターには以下の情報が含まれます。
<?xml version="1.0" encoding="utf-8"?>
<errinfo />
errorString パラメーターに、整形式の XML として、推定待機時間と実際の待機時間を返すようにする場合、GetJobCompletionState からの xmlError 出力に要素を追加し、アプリケーションで XML 用の Linq またはXmlReader を使用して、結合された statusOut 結果を解析できるようにします。
手順 4. には、UsingQueueSystem アプリケーションからの結果の例が含まれます。Project Server を新しくインストールする場合や Project Server をほとんど使用していない場合、キューで QueueCreateProject ジョブを処理する時間の初期見積もりは、多くの場合、非常に高くなります。Project Server がジョブを継続的に処理するにつれて、GetJobWaitTime からの見積もりの正確性は向上します。
手順 4: テスト アプリケーションを実行し、出力を確認するには
UsingQueueSystem.exe –writeLog を実行します。これにより、QueueUtilities メソッドで使用されるデータセット コンテンツが含まれるログ ファイルが書き込まれます。タイムアウト、プロジェクト名、および追加されるタスク名で既定の値を使用するには、他のパラメーターを含めないでください。図 1 は、コンソール出力を示しています。QueueStatusDataSet オブジェクトおよび QueueStatusRequestDataSet オブジェクト内のすべての値を表示するには、QueueOutput.log ファイルを開きます。
図 1. UsingQueueSystem アプリケーションのコンソール出力
図 1 の青緑色のコンソール出力は、QueueUtilities クラス内のメソッドからの出力です。推定のジョブ待機時間は、ProjectCreate メッセージで 1 秒です。ProjectPublish メッセージでは、推定のジョブ待機時間は 0 秒です。GetJobWaitTime メソッドは整数を返すため、待機時間が 1/2 秒よりも小さい場合、返される値は 0 になります。
QueueCreateProject を呼び出した後、アプリケーションは、errorString パラメーターを使用しない最初の WaitForQueue オーバーロードを使用します。QueuePublish を呼び出した後、アプリケーションは、errorString 出力を返す WaitForQueue オーバーロードを使用します。
図 1 のキュー ジョブの出力が示していることは、Project Server キューによって、ProjectCreate メッセージと ProjectPublish メッセージの CorrelationGUID が同じ値に内部的に設定されていることです。この 2 つのメッセージの JobGroupGuid 値も同じです。
UsingQueueSystem.exe –noQueue を実行します。プロジェクトの作成と発行の依存関係 GUID は同じです。また、ジョブ グループの GUID は同じです。したがって、QueuePublish を呼び出す前に、プロジェクトの作成が終了するまでキューを待機する必要はありません。ただし、テスト アプリケーションでは、その後、CheckOutProject が呼び出されます。
図 2 は、プロジェクトが作成されていることを示しています。ただし、CheckOutProject の呼び出し結果は、エラー 1028 を伴う FaultException です (ProjectDoesNotExist)。Project Web App、ProjTool、または Project Professional 2010 を使用して、プロジェクトが実際に作成されて発行されたこと、およびプロジェクトが依然として存在することを確認します。ただし、アプリケーションで CheckOutProject が呼び出される前に、ProjectCreate プロセスと ProjectPublish プロセスは完了していません。
図 2. -noQueue パラメーターを使用したコンソール出力
[サービス] ウィンドウを開き、Microsoft Project Server Queue Service 2010 を停止して、UsingQueueSystem.exe –writeLog –timeout 10 を実行します。
キューが実行されていない間、テスト アプリケーションはプロジェクトを作成できません。10 秒後に WaitForQueue メソッドがタイムアウトすると、アプリケーションは、QueuePublish を呼び出して、ProjectDoesNotExist エラーを取得します。
[サービス] ウィンドウで、Project Server Queue Service をオンに戻します。手順 3. で UsingQueueSystem.exe を実行するときにキュー サービスはオフになっていましたが、ProjectCreate と ProjectPublish のメッセージはキューに依然として追加されています。キュー サービスが開始されると、メッセージが処理され、プロジェクトの作成と発行が行われます。
ただし、プロジェクトには、追加されたタスクではなく、2 つのタスクのみが含まれます。キューがオフの間に、テスト アプリケーションを実行すると、テスト アプリケーションは作成メッセージと発行メッセージを追加しますが、プロジェクトをチェックアウトして 3 番目のタスクを追加することはできません。
UsingQueueSystem –projName "Duplicate Project Name" を 2 度実行します。1 度目の実行では、アプリケーションはプロジェクトの作成と発行を正常に行い、3 番目のタスクを追加します。2 度目の実行では、QueueCreateProject メソッドでエラー例外が発生し、エラー ProjectNameAlreadyExists が出力されます。ProjectCreate メッセージがポストされてないため、キュー サービスは関係ありません。
ジョブの状態の確認やジョブの取り消しを行う QueueSystem メソッドを使用するには、ユーザーに ManageQueue グローバル アクセス許可があるか、ユーザーがジョブを所有している必要があります。
UsingQueueSystem のテスト アプリケーションでは、異なるジョブ グループ内の PSI メソッドの呼び出しが前のキュー ジョブ グループの正常終了に依存する場合にのみ、Project Server Queue Service を待機する必要があることが示されています。一連の PSI キュー メソッドでジョブ グループと依存関係 GUID が一致する場合、Project Server は、キュー メッセージの順次処理を内部的に管理します。Project Server Queue Service が実行されていなくても、Project Server はキューにメッセージを追加し、その後、キュー サービスが再度実行されたときに、メッセージを処理します。