Recently we found a bug in our server-side Project Server code. Our goal was to set some project-related enterprise custom field values. Before setting the values, we tested, if the project is checked-out (to another user), and if so, we forced a a check-in, to be able to check out again to ourselves. After setting the values, we updated the project and published it, including check-in.
- if (proj.IsCheckedOut)
- {
- proj.Draft.CheckIn(true);
- }
- DraftProject draftProj = proj.CheckOut();
- // set some custom field values
- draftProj.SetCustomFieldValue("customFieldName", "customFieldValue");
- draftProj.Update();
- draftProj.Publish(true);
If the project was not checked-out, the code worked as expected. However, if the project was checked-out to a use, we got an exception despite of the test on the line
DraftProject draftProj = proj.CheckOut();
The exception was:
Microsoft.ProjectServer.PJClientCallableException was unhandled
_HResult=-2146233088
_message=CICOCheckedOutInOtherSession
HResult=-2146233088
IsTransient=false
Message=CICOCheckedOutInOtherSession
Source=Microsoft.ProjectServer
PSErrorCode=10103
PSErrorName=CICOCheckedOutInOtherSession
StackTrace:
at Microsoft.ProjectServer.PublishedProject.CheckOut()
at SPGeneral.Program.Main(String[] args) in c:\projects\PSTest\Program.cs:line 37
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
InnerException:
The message “CICOCheckedOutInOtherSession” suggested, that the project is not yet checked-in, although our traces showed that the line proj.Draft.CheckIn(true) was executed.
To understand the source of the problem, we should first understand how the CheckIn / CheckOut and Publish methods are executed:
Both of the CheckIn and Publish methods of the DraftProject class are executed asynchronously. These methods return a QueueJob object. The actual job is performed by the queue subsystem of Project Server.
The CheckOut method of the PublishedProject class is executed synchronously, it returns a DraftProject instance immediately.
In the above sample it means, that proj.CheckOut() is called, before the project would be effectively checked-in as a result of executing proj.Draft.CheckIn(true).
How to solve the problem, keeping our code readable if possible?
If you are working with client-side object model of Project Server, you know probably, that there is a solution for such issues: the WaitForQueue method of the ProjectContext object.Unfortunately, the equivalent of this method is not implemented in the server-side object model. It’s pretty strange, as usually its opposite is the case: methods and properties available on the server-side are many times missing on the client-side OM.
No problem, we can implement a similar method for ourselves!
The WaitForQueue method “wait for the specified queue job to complete, or for a maximum number of seconds”. It returns a JobState object, that should be JobState.Success if the queue job succeeded. If we have a look at the implementation of the method, we can see, that it calls the internal IsPendingJob method of the ProjectContext object to compare the current state of the job with the expected values, the refresh the job status by polling the server side objects. The wait time is decremented by 2 seconds in every iteration, the IsPendingJob method is responsible to sleep the thread for this two seconds.
Note: It means that the maximum wait time specified when calling WaitForQueue method is only an approximate value, as it does not include the send / response time of the server requests involved in the refreshing the job status, for example a one-minute wait time means 30 iterations, that is 30 requests / responses. So don’t be surprise if your wait times are considerably longer than specified if you have a slow network or busy server.
After this theory, lets see our own implementation:
- public static bool IsPending(this QueueJob job, out QueueConstants.JobState state)
- {
- state = job.JobState;
- switch (state)
- {
- case QueueConstants.JobState.Unknown:
- case QueueConstants.JobState.ReadyForProcessing:
- case QueueConstants.JobState.SendIncomplete:
- case QueueConstants.JobState.Processing:
- case QueueConstants.JobState.ProcessingDeferred:
- case QueueConstants.JobState.OnHold:
- case QueueConstants.JobState.Sleeping:
- case QueueConstants.JobState.ReadyForLaunch:
- Thread.Sleep(new TimeSpan(0, 0, 2));
- return true;
- }
- state = QueueConstants.JobState.Success;
- return false;
- }
- public static QueueConstants.JobState WaitToFinish(this QueueJob job, int timeoutSeconds)
- {
- QueueConstants.JobState state = QueueConstants.JobState.Unknown;
- while ((timeoutSeconds > 0) && job.IsPending(out state))
- {
- timeoutSeconds -= 2;
- }
- return state;
- }
As you can see, instead of extending the PSContext object with a WaitForQueue method, I decided to extend the QueueJob object itself with a WaitToFinish method. It seems to me simply more appropriate.
Note: As you probably know, and as I mentioned in my former posts, the server-side object model is based on the PSI infrastructure. It means, that the extra wait time mentioned above may apply to this solution as well.
Note 2: To be able to use the JobState enumeration value in your code, you should reference the Microsoft.Office.Project.Server.Library assembly in your project.
The modified logic in our application is displayed below:
- var timeOutInSec = 60; // wait max a minute
- bool canCheckOut = !proj.IsCheckedOut;
- if (!canCheckOut)
- {
- // Project is checked out. Forcing check-in.
- var job = proj.Draft.CheckIn(true);
- var jobState = job.WaitToFinish(timeOutInSec);
- if (jobState == QueueConstants.JobState.Success)
- {
- canCheckOut = true;
- }
- else
- {
- // WARNING Time-out on project check-in, or job failed
- }
- }
- if (canCheckOut)
- {
- DraftProject draftProj = proj.CheckOut();
- // set some custom field values
- draftProj.SetCustomFieldValue("customFieldName", "customFieldValue");
- draftProj.Update();
- // Publishing project (incl. check-in!)
- var job = draftProj.Publish(true);
- var jobState = job.WaitToFinish(Constants.DefaultPSJobTimeOut);
- if (jobState == QueueConstants.JobState.Success)
- {
- // Project checked-in + published.
- }
- else
- {
- // WARNING Time-out on project publish / check-in or job failed
- }
- }
- else
- {
- // WARNING Project can not be checked-out / processed
- }