In my recent post I wrote about a project publishing issue that was a result of a scheduling conflict.
The other day we had a similar problem with project publishing, but in this special case failed an other sub-process of the publishing process, the task synchronization. Another important difference from the former one is that at the scheduling conflict it was an end-user issue (a business user caused the conflict in the project plan scheduling), and in the case I’m writing about now, it was a mistake of an administrator plus a suboptimal code block in Project Server, that we can consider as a bug as well. But more on that a bit later…
First the symptoms we experienced. On the Manage Queue Jobs page in our PWA (http://YourProjectServer/PWA/_layouts/15/pwa/Admin/queue.aspx) we saw an entry of Job Type “SharePoint Task List Project” and Job State “Failed And Blocking Correlation”.
Clicking on the entry displayed this information:
Queue: GeneralQueueJobFailed (26000) – ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage. Details: id=’26000′ name=’GeneralQueueJobFailed’ uid=’46918ff3-3719-e611-80f4-005056b44e32′ JobUID=’adcad466-44bd-444b-a803-073fd12a2426′ ComputerName=’4fc61930-ef50-461b-b9ef-084a666c61ca’ GroupType=’ManagedModeTaskSynchronization’ MessageType=’SynchronizeTaskListInManagedModeMessage’ MessageId=’1′ Stage=” CorrelationUID=’cd56b408-a303-0002-d428-98cd03a3d101′.
The corresponding entries in the ULS logs:
PWA:http://YourProjectServer/PWA, ServiceApp:ProjectServerApplication, User:i:0#.w|YourDomain\FarmAccount, PSI: [QUEUE] SynchronizeTaskListInManagedModeMessage failed on project 5c21bf1b-c910-e511-80e5-005056b44e34. Exception: System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.UpdateAssignedToField(SPWeb workspaceWeb, DataSet taskDS, Guid taskUID, SPListItem listItem) at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.SynchronizeTask(SPList list, DataSet taskDS, Dictionary`2 taskMapping, DataRow row, DataView secondaryView, Dictionary`2 redoEntries) at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.<>c__DisplayClass1.<SynchronizeTaskListI…
…nManagedMode>b__0(SPWeb workspaceWeb) at Microsoft.Office.Project.Server.BusinessLayer.Project.<>c__DisplayClass3d.<TryRunActionWithProjectWorkspaceWebInternal>b__3c() at Microsoft.SharePoint.SPSecurity.<>c__DisplayClass5.<RunWithElevatedPrivileges>b__3() at Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated secureCode) at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(WaitCallback secureCode, Object param) at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(CodeToRunElevated secureCode) at Microsoft.Office.Project.Server.BusinessLayer.Project.TryRunActionWithProjectWorkspaceWebInternal(IPlatformContext context, Guid projectUid, Action`1 method, Boolean noThrow, DataRow row) at Microsoft.Office.Project.Server.Busine…
…ssLayer.ProjectModeManaged.SynchronizeTaskListInManagedMode(Guid projectUid) at Microsoft.Office.Project.Server.BusinessLayer.Queue.ProcessPublishMessage.ProcessSynchronizeTaskListInManagedModeMessage(Message msg, Group messageGroup, JobTicket jobTicket, MessageContext mContext), LogLevelManager Warning-ulsID:0x000CE687 has no entities explicitly specified.
So we have a NullReferenceException in the UpdateAssignedToField method of the Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged class (Microsoft.Office.Project.Server assembly).
From the job message type “ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage” it was obvious, that we have an issue with the synchronization between the project tasks and the Tasks list of the Project Web Site (PWS) of the project having the ID “5c21bf1b-c910-e511-80e5-005056b44e34”, and from the method name “UpdateAssignedToField” we could assume, that the problem is caused either by an existing value of the “Assigned To” field, or by constructing a new value we want to update the field with.
We can use the following script to find out, which PWS belongs to the project ID above:
$pwa = Get-SPWeb http://YourProjectServer/PWA
$pwa.Webs | ? { $_.AllProperties[‘MSPWAPROJUID’] -eq ‘5c21bf1b-c910-e511-80e5-005056b44e34’ }
If we have a look at the code of the UpdateAssignedToField method, we see it begins with these lines. These lines are responsible for removing users from the “Assigned To” field (of type SPFieldUserValueCollection) that are no longer responsible for the task. The second part of method (not included below) is responsible for inserting new user entries. I highlighted the line that may cause (and in our case in fact has caused) an error if the value of the assignedTo[i].User expression is null.
bool isModified = false;
SPFieldUserValueCollection assignedTo = listItem["AssignedTo"] as SPFieldUserValueCollection;
DataRowView[] source = taskDS.Tables[1].DefaultView.FindRows(taskUID);
if (assignedTo != null)
{
for (int i = assignedTo.Count – 1; i >= 0; i–)
{
string userName = ClaimsHelper.ConvertAccountFormat(assignedTo[i].User.LoginName);
if (!source.Any<DataRowView>(resourceRow => (string.Compare(userName, resourceRow.Row.Field<string>("WRES_CLAIMS_ACCOUNT"), StringComparison.OrdinalIgnoreCase) == 0)))
{
assignedTo.RemoveAt(i);
isModified = true;
}
}
}
The expression may be null if the user it refers to was deleted from the site. Note, that the expression assignedTo[i].LookupId even in this case returns the ID of the deleted user, and the expression assignedTo[i].LookupValue return its name.
How to detect which projects and which users are affected by the issue? I wrote the script below to display the possible errors:
- $rootWeb = Get-SPWeb http://YourProjectServer/PWA
- $rootWeb.Webs | % {
- $web = $_
- Write-Host ——————————-
- Write-Host $web.Title
- $foundMissingUsers = New-Object 'Collections.Generic.Dictionary[int,string]'
- $list = $web.Lists["Tasks"]
- if ($list -ne $null)
- {
- $list.Items | % {
- $_["AssignedTo"] | ? {
- ($_.User -eq $null) -and (-not $foundMissingUsers.ContainsKey($_.LookupId)) } | % {
- if ($_ -ne $null ) { $foundMissingUsers.Add($_.LookupId, $_.LookupValue) }
- }
- }
- $foundMissingUsers | % { $_ }
- }
- }
Assuming
$allUserIds = $rootWeb.SiteUsers | % { $_.ID }
we could use
$allUserIds -NotContains $_.LookupId
instead of the condition
$_.User -eq $null
in the script above.
Indeed, we could identify two users on two separate projects, that were deleted by mistake, although they have assignments in the project Tasks lists.
We have recreated the users (and assigned the new users to the corresponding enterprise resources), but they have now another IDs. What can we do to fix the problem? The synchronization does not work anymore on these projects (making the project publishing impossible as well) so it does not provide a solution. We could replace the users in the “Assigned To” field, or simply remove the wrong one (it would be re-inserted by the second part of the UpdateAssignedToField method during the next synchronization), but there is an event receiver (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler) registered on this list, that cancels any changes in the list items when you want to persist the changes via the Update method. To avoid that, we could temporary disable the event firing, as described here.
We used the following script to fix the errors.
- $rootWeb = Get-SPWeb http://YourProjectServer/PWA
- $siteUsers = $rootWeb.SiteUsers
- # disable event firing to prevent cancelling updates by PreventEdits method (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler)
- # http://sharepoint.stackexchange.com/questions/37614/disableeventfiring-using-powershell
- $receiver = New-Object "Microsoft.SharePoint.SPEventReceiverBase"
- $type = $receiver.GetType()
- [System.Reflection.BindingFlags]$flags = [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic
- $method = $type.GetMethod("DisableEventFiring", $flags)
- $method.Invoke($receiver, $null)
- $rootWeb.Webs | ? { $_.Title -eq 'YourProjectName' } | % {
- $web = $_
- Write-Host ——————————-
- Write-Host $web.Title
- $userPairs = ((122, 3421), (145, 2701))
- $userPairsResolved = $userPairs | Select-Object -Property `
- @{ Name="OldUserId"; Expression = { $_[0] }},
- @{ Name="NewUser"; Expression = { $up = $_; $siteUsers | ? { $_.ID -eq $up[1] } }}
- $list = $web.Lists["Tasks"]
- if ($list -ne $null)
- {
- $list.Items | % { $list.Items | % {
- $item = $_
- [Microsoft.SharePoint.SPFieldUserValueCollection]$assignedTo = $item["AssignedTo"]
- if ($assignedTo -ne $null)
- {
- $isModified = $false
- # iterate through the assignments
- for($i = 0; $i -lt $assignedTo.Count; $i++)
- {
- if ($assignedTo[$i].User -eq $null)
- {
- $userName = $assignedTo[$i].LookupValue
- $userid = $assignedTo[$i].LookupId
- $taskTitle = $item.Title.Trim()
- Write-Host Task """$taskTitle""" – assigned user """$userName""" "($userId)" missing
- $newUser = $userPairsResolved | ? { $_.OldUserId -eq $userid } | % { $_.NewUser }
- if ($newUser -ne $null)
- {
- $newUserId = $newUser.Id
- $newUserName = $newUser.Name
- do { $replaceAssignedTo = Read-Host Would you like to replace the assignment of the missing user with """$newUserName""" "($newUserId)"? "(y/n)" }
- until ("y","n" -contains $replaceAssignedTo )
- if ($replaceAssignedTo -eq "y")
- {
- # step 1: removing the orphaned entry
- $assignedTo.RemoveAt($i)
- # step 2: create the replacement
- [Microsoft.SharePoint.SPFieldUserValue]$newUserFieldValue = New-Object Microsoft.SharePoint.SPFieldUserValue($web, $newUser.Id, $newUser.Name)
- $assignedTo.Add($newUserFieldValue)
- # set the 'modified' flag
- $isModified = $true
- }
- }
- else
- {
- Write-Host WARNING No user found to replace the missing user with -ForegroundColor Yellow
- }
- }
- }
- # update only if it has been changed
- if ($isModified)
- {
- $item["AssignedTo"] = $assignedTo
- $item.Update()
- Write-Host Task updated
- }
- }
- }}
- }
- }
- # re-enabling event fireing
- $method = $type.GetMethod("EnableEventFiring", $flags)
- $method.Invoke($receiver, $null)
The variable $userPairs contains the array of old user ID – new user ID mappings. In step 1 we remove the orphaned user entry (the one referring the deleted user), in step 2 we add the entry for the recreated user. If you plan to run the synchronization (for example, by publishing the project) after the script, step 2 is not necessary, as the synchronization process inserts the references for the users missing from the value collection.
Note 1: The script runs only on the selected project (in this case “YourProjectName”), to minimize the chance to change another project unintentionally.
Note 2: The script informs a user about the changes it would perform, like to replace a reference to a missing user to another one, and waits a confirmation (pressing the ‘y’ key) for the action on behalf on the user executes the script. If you have a lot of entries to change, and you are sure to replace the right entries, you can remove this confirmation and make the script to finish faster.