3

Using PowerShell Background Jobs can help you speed up Exchange Tasks. (Part 2)

In Part 1 of this series. We looked at Background Jobs and how we can use them in our scripts. Commands like Start-Job, Receive-Job etc. works great but the resource usage can sometimes be a lot to handle.

In this second part, I will try to show a different approach to the same script that will use less resource and same results at the same speed or sometimes even better. We will be looking at Runspaces and RunspaceFactory.

The System.Management.Automation.Runspaces namespace contains the classes, enumerations, and interfaces used to create an individual runspace or a pool of runspaces. In Windows PowerShell 1.0, a single runspace is the operating environment in which one or more pipelines invoke cmdlets. In Windows PowerShell 2.0, a runspace or a pool of runspaces is the operating environment where the command pipeline of the PowerShell object is invoked.

and

RunspaceFactory Class Provides a means to create a single runspace or a pool of runspaces.

This means that we can use the Runspaces namespace create a RunspaceFactory and put our jobs in this RunspaceFactory so we can run them using asynchronous method to achieve better performance and resource usage.

This might be a bit confusing if you are new to PowerShell but I will try to explain all the code line by line again to give a better idea of what is going on.

Note: Be careful while using Runspaces as Exchange PowerShellMaxConcurrency is very easy to hit if you run jobs without getting them terminated. I usually recommend this method for scheduled tasks that will complete before anything else starts running.

Here is the code we are going to use for the same script from Part 1 but this time we are using RunspaceFactory.

 

#Our Function to split the mailboxes to smaller arrays
function Resize-Array ([object[]]$InputObject, [int]$SplitSize = 100)
{
	$length = $InputObject.Length
	for ($Index = 0; $Index -lt $length; $Index += $SplitSize)
	{
		, ($InputObject[$index .. ($index + $splitSize - 1)])
	}
}
#Get Credentials so we can use it to open sessions.
$Credentials = Get-Credential
#Set the ServerFQDN variable with the FQDN of the Exchange Server we will use to connect
$ServerFQDN = "EX01.Get-Mailbox.Org"
#Start the Clock
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()
#Get All Mailboxes
$AllMailboxes = Get-Mailbox -ResultSize Unlimited
#Calculate the split size that will be used to create 10 sessions
$SplitSize = $AllMailboxes.Count/10
#Split the array into 10 arrays
$MailboxesArray = Resize-Array -InputObject $AllMailboxes -SplitSize $SplitSize
#ScriptBlock that we will use for each Job
$ScriptBlock = {
	Param
	(
		[Parameter(Mandatory = $true)] $ServerFQDN,
		[Parameter(Mandatory = $true)] $Credentials,
		[Parameter(Mandatory = $true)] $Array
	)
	#Get the My Documents Path from local computer
	$DefaultRootFolder = [environment]::getfolderpath("MyDocuments")
	#Create a folder for collecting temp data
	$TargetDIR = "$DefaultRootFolder\SRKNVRGL_Temp"
	if (!(Test-Path -Path $TargetDIR))
	{
		New-Item -ItemType directory -Path $TargetDIR
	}
	#Set Temp location for powershell sessions to this temp folder
	Set-Location $TargetDIR
	$env:Temp = $TargetDIR
	$env:Tmp = $TargetDIR
	#Create a new ps session to exchange server
	$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://$ServerFQDN/PowerShell/ -Authentication Kerberos -Credential $Credentials -WarningAction 'SilentlyContinue'
	#Connect and import module just incase if you are running it from Powershell and not from EMS
	$Connect = Import-Module (Import-PSSession $Session -AllowClobber -WarningAction 'SilentlyContinue') -Global -WarningAction 'SilentlyContinue'
        #Create an empty array for Calendar Permissions.
        #This will be our overall result
	$CalendarPermissions = @()
        #We will get calendar permissions for each mailbox
	foreach ($Mailbox in $Array)
	{
		#Set the Default calendar name to show up as Calendar on output.
		$Calendar = 'Calendar'
		#Get the calendar permission for this mailbox
		$CalendarPermission = Get-MailboxFolderPermission -Identity ($Mailbox.alias + ':\Calendar') -User Default -ErrorAction 'SilentlyContinue' | Select User, AccessRights, Identity
		#If the calendar name is not calendar the return will be empty.
		#If return is empty we need to check all calendar folders.
		if (!$CalendarPermission)
		{
			#If the return is empty find the calendar in the mailbox as the name might be localized.
			#Find the calendar folders
			$Calendars = Get-MailboxFolderStatistics -Identity $Mailbox.Identity -FolderScope Calendar | ? { $_.FolderType -eq "Calendar" } | Select -Expand Name
			#Create empty calendar permission array as we will go to every calendar folder and collect permissions.
			$CalendarPermission = @()
			#Go and check every calendar folder in the mailbox
			foreach ($Calendar in $Calendars)
			{
				#Get the calendar permissions on this calendar folder
				$CalendarPermission = Get-MailboxFolderPermission -Identity "$($Mailbox.Identity):\$($Calendar)" -User Default -ErrorAction 'SilentlyContinue' | Select User, AccessRights, Identity
				#Create a result object that includes our results
				$result = New-Object PSObject -Property @{ "Mailbox" = $Mailbox.Name; "User" = $CalendarPermission.User; "AccessRights" = $CalendarPermission.AccessRights; "Calendar" = $Calendar }
				#Add the result to Calendar Permissions array
				$CalendarPermissions += $result
			}
		}
		#if the Calendar folder exists than add the results to Calendar Permissions array
		else
		{
			$result = New-Object PSObject -Property @{ "Mailbox" = $Mailbox.Name; "User" = $CalendarPermission.User; "AccessRights" = $CalendarPermission.AccessRights; "Calendar" = $Calendar }
			$CalendarPermissions += $result
		}
	}
	#Output the CalendarPermissions so we can collect once the job is completed.
	Return $CalendarPermissions
}
#Create a Runspace Pool with min 1 and max 11 async jobs to run
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 11)
#Open the Pool
$RunspacePool.Open()
#We will keep our Jobs in an array to be able to manage them
$Jobs = @()
#For each Smaller array we have in MailboxesArray we will create a job
foreach ($Array in $MailboxesArray)
{
       #Create a Job with Script Block and add ServerFQDN as the first argument that will be used for the script
	$Job = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddArgument($ServerFQDN)
	#Add the Credentials Argument
	$Job = $Job.AddArgument($Credentials)
	#Add the smaller array of our mailboxes as Argument
	$Job = $Job.AddArgument($Array)
	#Set the jobs Runspace Pool
	$Job.RunspacePool = $RunspacePool
	#Add the job to array
	$Jobs += New-Object PSObject -Property @{
		#Set the pipeline we are goint to use
		Pipe = $Job
		#BeginInvoke will start our job.
		Result = $Job.BeginInvoke()
	}
}
#Wait until all jobs are completed and show completed job count on screen every 10 seconds
Do
{
	$Total = ($jobs).count
	$Completed = ($jobs | select -expand result | ? { $_.iscompleted -eq $true }).count
	if ($Completed -gt 0)
	{
		Write-Host "$($Completed)-" -NoNewLine
	}
	else
	{
		Write-Host "." -NoNewline
	}
	Start-Sleep -Seconds 10
}
While (($jobs | select -expand result | ? { $_.iscompleted -eq $false }).count -gt 0)
#$Results will be our total result
$Results = @()
#Collect the results for each job and add it to $Results
ForEach ($Job in $Jobs)
{
	$Pipe = $Job | Select -Expand Pipe
	$Result = $Job | Select -Expand Result
	$Results += $Pipe.EndInvoke($Result)
}
#Dispose and Close the Runspace
$RunspacePool.Dispose()
$RunspacePool.Close()
write-host "Total Elapsed Time: $($elapsed.Elapsed.ToString())"

While the script is still running, if we look at the task manager to see PowerShell processes, we only have 1:

4

And the result is:

5

So overall for our script the winner is Runspaces not because it had the exact same performance with Start-Job but it had better resource usage.

I hope this post gives you an idea about how to utilize PS Background jobs or Runspaces to improve your Exchange Scripts for performance and save you sometime.

Please drop a comment if you have questions or feedback.