Background Jobs and Recursive Functions

I recently had a thought: I have not written much on background jobs over the last nearly eight years. I should do more of that. Part of it stemmed from recently preparing an old post that was published elsewhere to be brought back to life here on tommymaynard.com. That one was Keeping a Continuous Total.

In that post, I wrote, “Next up is likely putting this code into a background job; it’s not the quickest thing I’ve written (although I blame that on the speed of the PowerShell Gallery lookup process, perhaps). Maybe a background job that runs in, or starts at, the end of the profile script. This, in order that these slow-to-obtain results are available sooner and with minimal impact.”

Well, I did that. While the function to do lookups in the PowerShell Gallery already existed in my profile script, I added a new function to my profile script that includes various background job commands that is invoked by my profile script. And, it turns out I was able to make it a recursive function, as well. You never think you will need one of those until the opportunity presents itself. I am so glad I noticed this opportunity.

Let’s start with my Show-PSGalleryProject function. You are welcome to use this as well. This function goes out to the PowerShell Gallery and determines the download count of each of my scripts and modules published there. This function takes some time to run, therefore, I want it to run in the background, so the results are available quicker than they would be otherwise.

Function Show-PSGalleryProject {
    [CmdletBinding()]
    Param (
        [System.Array]$Projects = ('TMOutput','Start-1to100Game3.0','Get-TMVerbSynonym',
            'SinkProfile','Show-PSDriveMenu','Switch-Prompt')
    )

    Foreach ($Project in $Projects) {
        If (Find-Module -Name $Project -ErrorAction SilentlyContinue) {
            $TempVar = Find-Module -Name $Project; $Type = 'Module'
        } ElseIf (Find-Script -Name $Project) {
            $TempVar = Find-Script -Name $Project; $Type = 'Script'
        }
        $TotalDownloads = [int]$TotalDownloads + [int]$TempVar.AdditionalMetadata.downloadCount
        [PSCustomObject]@{
            Name = $TempVar.Name
            Type = $Type
            Version = $TempVar.Version
            Downloads = $TempVar.AdditionalMetadata.downloadCount
            TotalDownloads = $TotalDownloads
        }
    } # End Foreach.
} # End Function: Show-PSGalleryProject.

The invocation of the above function is controlled by the below function. So, the Show-PSGalleryProject function goes out to the gallery to collect information, and the Show-PSGalleryProjectJob function orchestrates this process. Let me explain what this section function does. But first, take a look at it and see what you can extract yourself.

Set-Alias -Name psgal-Value Show-PSGalleryProjectJob
Function Show-PSGalleryProjectJob {
	$FunctionName = $MyInvocation.MyCommand.Name
	if (-Not(Get-Job -Name $FunctionName -ErrorAction SilentlyContinue)) {
		Start-Job -Name $FunctionName -ScriptBlock ${Function:Show-PSGalleryProject} | Out-Null
	} else {
		if ((Get-Job -Name $FunctionName).State -eq 'Completed') {
			$JobEndTime = (Get-Job -Name $FunctionName).PSEndTime
			Receive-Job -Name $FunctionName |
				Select-Object -Property *,@{Name='EndTime'; Expression={$JobEndTime}} -ExcludeProperty RunspaceId |
				Format-Table -AutoSize
			Remove-Job -Name $FunctionName
			& $FunctionName
		} else {
			Write-Warning -Message "Please wait. The $FunctionName background job is $((Get-Job -Name Show-PSGalleryProjectJob).State.ToLower()) (Id: $((Get-Job -Name Show-PSGalleryProjectJob).Id))."
		}
	}
}
psgal

Buckle up; here we go.

Line 1: Create the psgal alias for the function.
Line 2 (and 18): Define the Show-PSGalleryProjectJob function.
Line 3: Create the $FunctionName variable and assign it the name of this function using the $MyInvocation variable. The function’s name is used repeatedly throughout the function, so it made sense to store it in a variable.
Line 4: Include an if-else language construct. The if portion determines if there is a background job called Show-PSGalleryProjectJobor not.
Line 5: If that background job does not exist, it should be created and started. The Start-Job cmdlet’s ScriptBlock property invokes the Show-PSGalleryProject function.
Line 6: This is the beginning of else portion. The lines beneath it will run if there is already a background job running called Show-PSGalleryProjectJob.
Line 7 – 13: Nested in the else portion is another if-else construct. If the job is complete we run the commands in the if portion. If the job is not yet complete, we run the commands in the else portion below. The if portion does all of the following: it collects the end time of the job and stores it in $JobEndTime, it uses Receive-Job to collect the results of the completed background job, it pipes those results to Select-Object and displays all the default properties, as well as the end time we add using a calculated property. It takes those results and pipes them to Format-Table -AutoSize . Once it is done with those steps, it uses Remove-Job to remove/delete the background job.

Now, for the recursion. The final step inside the if portion of this nested if-else is to invoke the Show-PSGalleryProjectJob function. That is right. The function invokes or calls itself. It starts this whole process over again. It does this using the call operator (&) and the $FunctionName variable. Remember, that variable holds the name of the function. Without the call operator, it would just echo the value in the variable. That operator also called the invocation operator, invokes the function again. Every time the job is completed and the values are returned, the process starts over. On a side note, I have written about recursive functions once before.

Line 14 – 16: The else portion issues a Write-Warning message indicating that the job is not yet complete.
Line 19: The alias invokes the Show-PSGalleryProjectJob function.

The below results show the functions working together. Consider background jobs for longer running tasks and consider recursive functions when a situation presents itself where a function should call, or invoke, itself.

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 1).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] # Here is where I waited from some time to pass...
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal

Name                Type   Version Downloads TotalDownloads EndTime
----                ----   ------- --------- -------------- -------
TMOutput            Module 1.1     2994                2994 2/10/2022 5:07:50 PM
Start-1to100Game3.0 Script 3.0     266                 3260 2/10/2022 5:07:50 PM 
Get-TMVerbSynonym   Script 1.4     293                 3553 2/10/2022 5:07:50 PM 
SinkProfile         Module 1.0     304                 3857 2/10/2022 5:07:50 PM 
Show-PSDriveMenu    Script 1.1     186                 4043 2/10/2022 5:07:50 PM 
Switch-Prompt       Script 1.2.0   236                 4279 2/10/2022 5:07:50 PM 

[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\] 
[PS7.2.1][C:\] psgal
The Show-PSGalleryProjectJob background job is running (Id: 3).
[PS7.2.1][C:\]

Leave a Reply

Your email address will not be published. Required fields are marked *