다음을 통해 공유


How to: Use the QueueSystem Service

Applies to: Office 2010 | Project 2010 | Project Server 2010 | SharePoint Server 2010

Microsoft Project Server 2010 includes the Project Server Queue Service to handle asynchronous jobs that can be long-running and to manage many simultaneous jobs. Project Server applications often have to determine when a job that was sent to one of the Project Server queues is complete, to then perform an action that depends on the success of another action. This article shows how to develop and use a utility class with the QueueSystem service of the Project Server Interface (PSI), for testing actions of the Project Server queue.

This article is an update of the How to: Use the QueueSystem Web Service article that was written for Microsoft Office Project Server 2007. There are many ways to check the Project Server queue for job completion. The example in this article, which outputs queue status and job status, is designed for a test installation that helps to show how the Project Server queue works. The complete UsingQueueSystem code example in the Project 2010 SDK download includes additional overloads of the WaitForQueue method. For a link to the download, see the Project Developer Center.

This article includes the following sections:

  • Using GetJobCompletionState to Wait for a Queue Job

  • Using ReadJobStatus to Wait for a Job Group

  • Testing the Queue Utility Methods

  • Running the Test Application

The PSI includes many methods that begin with the name Queue, such as QueueSubmitTimesheet, QueueArchiveCustomFields, and QueueCreateProject. All asynchronous PSI methods use the Project Server Queue Service and have names that begin with Queue. If the PSI method name does not begin with Queue, the method is synchronous and does not use the Queue service. The Microsoft .NET Framework automatically adds asynchronous methods when wsdl.exe generates the web service proxies; those auto-generated proxy methods do not use the Project Server Queue Service.

The Manage Queue Jobs page in Project Web App (https://ServerName/ProjectServerName/_layouts/pwa/Admin/queue.aspx) uses the QueueSystem methods to filter and list jobs, and to choose columns for the Jobs Grid. Project Server maintains two queues, both of which are managed by the Microsoft Project Server Queue Service 2010 service:

  • The Timesheet queue manages jobs from queue-based methods in the Timesheet service.

  • The Project queue manages jobs such as create, update, save, and publish from the other queue-based web methods.

The QueueSystem class in the PSI includes methods that estimate how long a job is going to take, find the job status, and cancel a job. The queue-based PSI methods send a message with a job ID to the Project Server Queue Service to process a job. If the method spawns only one queue job, the method parameters include jobUid, which can be used for finding the job status in the queue. Some methods, such as QueueArchiveProjects and QueueCleanUpTimesheets, spawn more than one queue job; they do not expose the jobUid parameter.

To track PSI methods that spawn more than one queue job, you can use any of the methods that return a QueueStatusDataSet, such as ReadMyJobStatus.

Correlated jobs are queue jobs that are sequentially related to each other. For correlated jobs, the queue creates a job correlation GUID that you can use with methods such as GetJobCount, UnblockCorrelation, RetryCorrelation, GetProposedJobWaitTime, QueuePurgeArchivedJobs, and CancelCorrelation. For example, when you use the QueueCreateProject method and the QueuePublish method to create and publish a project, the Project Server Queue Service sets the CorrelationGUID property to the value of the project GUID.

Note

Because the correlation GUID is the same for the create job and the publish job, and Project Server internally adds both jobs to the same job group, it is not necessary for an application to poll the queue and wait for the create job to finish before starting the publish job. The Project Server Queue Service automatically handles the sequence for queue jobs that have the same correlation GUID within a job group.

To track multiple jobs that are not correlated, you can set a tracking GUID and use it to track the job group in the queue. For an example of how to set a tracking GUID on multiple jobs, see ReadJobStatus. For other examples that use a tracking GUID, see GetJobGroupWaitTime and GetJobGroupWaitTimeSimple.

This article shows how to create a queue utilities class and methods that determine when a specified queue job is either complete or has exceeded a specified time, or shows when the job has an error condition in the Project Server Queue Service.

Important

Many of the code examples in SDK topics for queue-based methods, such as QueueDeleteProjects, include a simpler WaitForQueue method that uses Thread.Sleep between checks for the queue job status. Blocking execution of a process in the server thread pool for a long duration or for many simultaneous processes, such as submitting timesheets, can severely affect the performance of Project Server. We recommend the Thread.Sleep method only for limited use in client applications, not for components that run on the server.

Middleware components that run on the Project Server computer should not use Thread.Sleep in a tight loop. Instead, an application that uses a server component for calls to PSI queue-based methods should display the job status and let the user refresh the application to update the job status field. The WaitForQueue methods in the code examples of this article use the CurrentThread.Join method, which blocks the calling thread for a specified number of milliseconds, or until the thread terminates, but allows other thread processes to continue.

The sample QueueUtilities class includes a TimeoutSec property so that you can specify the maximum number of seconds to wait. The sample code returns the job status, and can return an error condition. To help show how the Project Server queue works, the WaitForQueue method also returns the initial Project Server Queue Service estimate of the number of seconds to complete the job.

Using GetJobCompletionState to Wait for a Queue Job

Procedure 1 shows how to create a queue utilities class with an example WaitForQueue method that waits for a specified queue job. This WaitForQueue method uses the GetJobCompletionState method and the ReadJobStatusSimple method of the QueueSystem service.

Procedure 1. To create a utility method that waits for a queue job

  1. In Microsoft Visual Studio 2010, create a console application named UsingQueueSystem. Add an application configuration file named app.config that defines endpoint address for the Project service and the QueueSystem service, as follows:

    <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>
    

    Change the server name and the Project Web App instance name for your Project Server installation.

  2. Add the following references to your application:

    • Microsoft.Office.Project.Server.Library

    • ProjectServerServices proxy assembly for WCF-based applications.

    For information about creating and using a proxy assembly for the PSI services, see Prerequisites for WCF-Based Code Samples.

  3. Add a class to your application that will contain the queue utilities. For example, name the class QueueUtilities. Add the class constructor, constants, variables, and class properties as follows:

    • The INCREMENTAL_SLEEPTIME constant should be the number of milliseconds it takes a typical short queue job to complete.

    • queueSystemClient contains the QueueSystemClient object in the WCF interface of the PSI.

    • logWriter contains the StreamWriter object for writing a log file.

    • The queueMessage variable is used to hold messages for the log file.

    • The TimeoutSec property is the maximum number of seconds to wait for the queue.

    • The WriteLogFile property specifies whether to write a log file for queue operations.

    • The TotalUnprocessedJobCount property uses the ReadAllJobStatusSimple method to get a QueueStatusDataSet with all queue jobs since 2007. The number of unprocessed jobs is the number of rows in the Status table where the JobCompletionState is ReadyForProcessing.

    • The QueueUtilities constructor initializes queueSystemClient, logWriter, TimeoutSec, and WriteLogFile.

    The following code implements the constructor and properties of the QueueUtilities class.

    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). */
        }
    }
    
  4. Add the methods that the WaitForQueue method will use. IsJobStateProcessed returns true if the current job state indicates that the queue has completed processing a job. In the test sample, LogQueueStatus, LogJobStatus, and LogQueueTimeout are used for output of testing information. The test output methods would probably not be used in a production application.

    /// <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;
    }
    
  5. Add a WaitForQueue method to the QueueUtilities class, which returns a JobState enumeration constant. The WaitForQueue method in this example uses a while (true) loop to check the queue job state, by using the GetJobCompletionState method. The first time through the loop, the GetJobWaitTime method shows the estimated wait time for the queue to process the job. WaitForQueue exits the loop and returns the job state when the queue service is finished processing the job, or if the job state is unknown, or when the process has exceeded the maximum wait time.

    The job state can be one of the JobState constants. If the job state is one of the states that are specified in the IsJobStateProcessed method, such as Success or Failed, the queue has finished processing the job. In this example, WaitForQueue gets the job status in a QueueStatusDataSet by using the ReadJobStatusSimple method. In this case, the job status is used only for output in the test application.

    In the following code, WaitForQueue has two overloads:

    • Parameters for the first overload are jobUid and errorString. The method outputs any error from the call to GetJobCompletionState.

    • The second overload of the WaitForQueue method has only the jobUid parameter. The method calls the first overload, and discards the error output.

    Note

    The complete solution in the Project 2010 SDK download includes several additional overloads of the WaitForQueue method, which wait for specified message types and job states.

    #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
    

Testing the Queue Utility Methods includes example code that uses both of the WaitForQueue overloads. Running the Test Application shows sample output.

Using ReadJobStatus to Wait for a Job Group

The sample WaitForQueueByGroup method uses a QueueStatusRequestDataSet as a parameter for the ReadJobStatus method, which returns a QueueStatusDataSet.

Procedure 2. To wait for a job group

  1. Add the WaitForQueueByGroup method, with the code to create the QueueStatusRequestDataSet and other variables for use in the while(true) loop. Add a row to the StatusRequest table for each job in the jobUids list.

    Note

    The WaitForQueueByGroup method can be simplified if you do not include the if (WriteLogFile) blocks for output of test data.

    /// <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 */
    }
    
  2. Add the while(true) loop. The loop calls ReadJobStatus and gets one row in the QueueStatusDataSet.Status table for each job in the QueueStatusRequestDataSet.

    The while (true) loop checks each job row in the QueueStatusDataSet.StatusRow table for the job state, and sets the isGroupSuccessful value according to comparisons with unsuccessful job states. WaitForQueueByGroup exits the loop when the queue completes processing of the jobs, and then returns the isGroupSuccessful value.

        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;
            }
        }
    

Testing the Queue Utility Methods includes example code that uses the WaitForQueueByGroup method for two jobs. Running the Test Application shows sample output.

Testing the Queue Utility Methods

UsingQueueSystem is a console application that instantiates a QueueUtilities object and tests the two WaitForQueue method overloads and the WaitForQueueByGroup method. The test application creates, saves, and publishes a project that has two tasks. The application then checks out the project, adds a task, and then checks the project back in.

Optional arguments for the UsingQueueSystem application enable you to run the tests without waiting for queue jobs, specify the maximum time to wait for the queue job, write a log file that contains the contents of the QueueStatusRequestDataSet and QueueStatusDataSet objects, and specify the name of the project and the name of the added task.

Procedure 3. To write the test application for the WaitForQueue methods

  1. In the Program.cs file of the UsingQueueSystem solution, add the using declarations, the class constants, the ParseCommandLine method, the ConfigClientEndpoints method, and the WriteFaultOutput method and the Helpers class to output FaultException data, as follows:

    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;
            }
        }
    }
    
  2. Add the CreateSampleProject method and the AddTask method.

    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;
    }
    
  3. Add the try – catch – finally blocks, which configure the client endpoints, instantiate a QueueUtilities object, create and publish the sample project, and then check out the project and add a task.

    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();
    }
    

If the test application waits for the queue, the CreateSampleProject method calls the QueueCreateProject method, and then calls the WaitForQueue overload with a new queue job GUID. When the queue job is finished, the application calls QueuePublish, and then calls the WaitForQueue overload that outputs an error string.

When the publish job is complete, the application calls CheckOutProject with a new queue session GUID to track multiple jobs, calls AddTask to create a ProjectDataSet that contains a new task, and then calls QueueAddToProject. Instead of waiting for the queue job at that point, the application adds the job GUID to the jobsInGroup list. Finally, the application calls QueueCheckInProject, adds a new queue job to the jobsInGroup list, and then calls WaitForQueueByGroup with the group of jobs and the queue session GUID.

Running the Test Application

The purpose of the UsingQueueSystem test application is to show an example of when it is necessary to wait for the queue, to show an example of when it is not necessary to wait for the queue, and to examine data that is used and returned by some of the QueueSystem methods.

The QueueUtilities class is a sample for test purposes. The GetJobCompletionState method returns error information in XML format in the errorString parameter. If there are no errors, the xmlError parameter contains the following:

<?xml version="1.0" encoding="utf-8"?>
<errinfo />

If you want the errorString parameter to return the estimated wait time and the actual wait time as well-formed XML, you could add elements to the xmlError output from GetJobCompletionState so that the application could parse the combined statusOut results by using Linq for XML or XmlReader.

Procedure 4 includes examples of results from the UsingQueueSystem application. The initial estimates of time for the queue to process a QueueCreateProject job are often high when Project Server is newly installed or seldom used. As Project Server continues to process jobs, the accuracy of the estimates from GetJobWaitTime improves.

Procedure 4: To run the test application and examine output

  1. Run UsingQueueSystem.exe –writeLog, which writes a log file that includes dataset contents used in the QueueUtilities methods. To use the default values for timeout, project name, and added task name, do not include the other parameters. Figure 1 shows the console output. To see all of the values in the QueueStatusDataSet objects and the QueueStatusRequestDataSet object, open the QueueOutput.log file.

    Figure 1. Console output of the UsingQueueSystem application

    Console output of UsingQueueSystem

    In Figure 1, console output in the teal (blue-green) color is from methods in the QueueUtilities class. The estimated job wait time is 1 second for the ProjectCreate message. For the ProjectPublish message, the estimated job wait time is 0 seconds. Because the GetJobWaitTime method returns an integer, if the wait time is shorter than ½ second, the value it returns is 0.

    After calling QueueCreateProject, the application uses the first WaitForQueue overload, which does not use the errorString parameter. After calling QueuePublish, the application uses the WaitForQueue overload that returns the errorString output.

    The queue job output in Figure 1 shows that the Project Server queue internally sets CorrelationGUID to the same value for both the ProjectCreate message and the ProjectPublish message. The JobGroupGuid value is also the same for both messages.

  2. Run UsingQueueSystem.exe –noQueue. Because the correlation GUID is the same for creating and publishing the project, and the job group GUID is the same, it is not necessary to wait for the queue to finish creating the project before calling QueuePublish. However, the test application then calls CheckOutProject.

    Figure 2 shows that the project is created, but the call to CheckOutProject results in a FaultException with error 1028, ProjectDoesNotExist. You can use Project Web App, ProjTool, or Project Professional 2010 to check that the project actually was created and published, and still exists. However, the ProjectCreate and ProjectPublish processes were not complete before the application called CheckOutProject.

    Figure 2. Console output using the -noQueue parameter

    Console output, using the -noQueue parameter

  3. Open the Services window, stop Microsoft Project Server Queue Service 2010, and then run UsingQueueSystem.exe –writeLog –timeout 10.

    The test application is not able to create a project while the queue is not running. When the WaitForQueue method times out after 10 seconds, the application calls QueuePublish, and gets a ProjectDoesNotExist error.

  4. In the Services window, turn the Project Server queue service back on. Although the queue service was off when you ran UsingQueueSystem.exe in step 3, the ProjectCreate and ProjectPublish messages were still added to the queue. When the queue service starts, it processes the messages, and creates and publishes the project.

    However, the project contains only two tasks, not the added task. When the test application runs while the queue is turned off, it adds the create message and the publish message, but cannot check out the project to add a third task.

  5. Run UsingQueueSystem –projName "Duplicate Project Name" twice. The first time, the application creates and publishes the project normally, and adds a third task. The second time, the QueueCreateProject method raises a fault exception, with the error ProjectNameAlreadyExists. The queue service is not involved, because the ProjectCreate message does not get posted.

To use the QueueSystem methods that check a job status or cancel a job, the user must have the ManageQueue global permission or own the job.

The UsingQueueSystem test application shows that it is necessary to wait for the Project Server Queue Service only if a call to a PSI method in a different job group depends on successful completion of a previous queue job group. If the job group and the correlation GUID match for a series of PSI queue methods, Project Server internally manages the sequential processing of queue messages. When the Project Server Queue Service is not running, Project Server still adds messages to the queue, and then processes the messages when the queue service is running again.

See Also

Reference

ReadMyJobStatus

ReadJobStatus

GetJobGroupWaitTimeSimple

Project Server Error Codes