Tracking the State of PowerShell Runspaces in a Runspacepool

A question that I have received a number of times has always involved a way to determine the state of each runspace that is in a runspacepool. Since a runspacepool is used to throttle the number of runspaces that are able to run at a single time, it only makes sense that there should be a way to determine what runspaces are currently running and what ones are in a ‘NotStarting’ state. Knowing when a runspace is completed is simple with the IsCompleted property of the PowerShellAsyncResult object, but beyond that, it has been a mystery.

If you use my PoshRSJob module, you may have noticed that after you begin all of the RSJobs, that they all show a state of ‘Running’, which can create some confusion even though some of the jobs are not actually running just yet. Between this issue with my module and the number of questions that I had received, it has been one of my goals with runspaces to find a way to determine the state of each runspace in a runspacepool so others can make use of whatever technique that I could find as well as adding the discovered solution into my module.

The solution that I found begins with using reflection techniques to dig deeper into the PowerShell instance that we created and used with our runspacepool to find the state of a running pipeline and then looks at the IsCompleted property of the PowerShellAsyncResult object to determine whether that particular instance is in fact running or waiting to start in the runspacepool. in order to properly test this and ensure that I am not missing anything, I will have to have at least one runspace that has already been completed, one that is going to always be running and another one that should not be running yet.

To make this happen, I will kick off 6 runspaces within a runspacepool that will only allow a maximum of 2 concurrently running runspaces. The scriptblock for those will contain a While loop that will only break if the value being checked is an odd number. This means that at some point 2 runspaces will be completed,  2 will continue to run and another 2 will be waiting to start, but will never start due to the other jobs continuously running as they contain even numbers.

My runspacepool job code is below and uses the ideas that I described to ensure that I meet my goal of having runspaces in various states.

$RSP = [runspacefactory]::CreateRunspacePool(1,2)
$RSP.Open()

$List = New-Object System.Collections.ArrayList

$Result = 1..6 | ForEach {
    $PS = [powershell]::Create()
    $PS.RunspacePool = $RSP
    [void]$PS.AddScript({
        Param($i)
        While ($True) {
            Start-Sleep -Milliseconds 1000
            If ($i%2) {BREAK}
        }
    }).AddArgument($_)
    $List.Add(([pscustomobject]@{
        Id = $_
        PowerShell = $PS
        Handle = $PS.BeginInvoke()
    }))
}

 

You will notice that I saved the results of the BeginInvoke() command as well as the Id and PowerShell instance. This way we can accurately tell what values we are checking against as the Id matches the value of the If statement.

The contents of the list looks like this:

image

Based on my code in each runspace, we can easily deduce that Ids 1 and 3 should be completed while 2 and 4 should be running which leaves 5 and 6 as the remaining jobs left to run. a quick look at the Handle property of this object should show the IsCompleted property being True for 1 and 3 whereas the others will show as False.

image

This was the easy part. Knowing what has already finished isn’t what I set out to solve as it was already easy to find. I am now going to take a long look at Id 2 to look for a running pipeline which should give me an indication that there is something already running in the runspacepool.

In this example, we will look at Id 2 (which would be $list[1]) and will see if this is truly running like I am thinking it is.

The first thing I need to pull is the System.Management.Automation.PowerShell+Worker object from the PowerShell instance. This is not publicly available to view so we now begin our first dive into using reflection to get this object.

$Flag = 'static','nonpublic','instance'
$_Worker = $list[1].PowerShell.GetType().GetField('worker',$Flag)
$Worker = $_Worker.GetValue($list[1].PowerShell)

Using the proper flags to tell what kind of values we want for the Field, we can locate the nonpublic field and then take that object and pull the value using GetValue() and providing the original object that it belongs to, in this case the original object is the PowerShell instance. We can verify that we have the object and then move onto the next property to locate.

image

We are now halfway down our reflection rabbit hole. Taking this PowerShell+Worker object, we will go down one more level deeper and pull the CurrentlyRunningPipeline property out of this which will tell us if…you guessed it… if the runspace is currently running.

$_CRP = $worker.GetType().GetProperty('CurrentlyRunningPipeline',$Flag)
$CRP = $_CRP.GetValue($Worker)
$CRP

image

Perfect! If there is data in this property, then that means that there is something running in the runspace and that therefore means the runspace in the runspacepool is currently running!

Ok, just a single test doesn’t really tell us that we are on the right path. We should definitely run this against a runspace that we know has already completed, such as Id 1 to check that piece and then run this against an Id that should not be running yet, like Id 5. If both of those do not have anything within the CurrentlyRunningPipeline property, then we can make a reasonable guess that we have a working approach to tracking runspace activity.

First, let’s run this against the completed runspace (Id1: $List[0]) and see what happens.

$Flag = 'static','nonpublic','instance'
$_Worker = $list[0].PowerShell.GetType().GetField('worker',$Flag)
$Worker = $_Worker.GetValue($list[0].PowerShell)

$_CRP = $worker.GetType().GetProperty('CurrentlyRunningPipeline',$Flag)
$CRP = $_CRP.GetValue($Worker)
$CRP

image

No data in the property on the completed runspace. Now onto the runspace that we believe to be waiting to start (Id5: $list[4]).

$_Worker = $list[4].PowerShell.GetType().GetField('worker',$Flag)
$Worker = $_Worker.GetValue($list[4].PowerShell)

$_CRP = $worker.GetType().GetProperty('CurrentlyRunningPipeline',$Flag)
$CRP = $_CRP.GetValue($Worker)
$CRP

image

Another successful test! Based on the results of both the completed and presumed pending runspace, we can see that we have a good way to determine the state of the runspaces. The only thing left to do is make sure that we know the difference between a completed and pending runspace and all we have to do is look at the IsCompleted property that we talked about earlier in the article. When it shows True and has no data in CurrentlyRunningPipeline, then it is a completed runspace but if the IsCompleted is False with no data in the other property, then we know that this hasn’t started yet.

The final script for my demo shows what I did to create the runspaces and then to determine the state of the runspaces and returning a ‘Running’, ‘NotStarted’ and ‘Completed’ state for each runspace.

$RSP = [runspacefactory]::CreateRunspacePool(1,2)
$RSP.Open()

$List = New-Object System.Collections.ArrayList

$Result = 1..6 | ForEach {
    $PS = [powershell]::Create()
    $PS.RunspacePool = $RSP
    [void]$PS.AddScript({
        Param($i)
        While ($True) {
            Start-Sleep -Milliseconds 1000
            If ($i%2) {BREAK}
        }
    }).AddArgument($_)
    $List.Add(([pscustomobject]@{
        Id = $_
        PowerShell = $PS
        Handle = $PS.BeginInvoke()
    }))
}
Start-Sleep -Seconds 2 
$Flag = 'static','nonpublic','instance' 
0..5 | ForEach {
    $_Worker = $list[0].PowerShell.GetType().GetField('worker',$Flag)
    $Worker = $_Worker.GetValue($list[$_].PowerShell)

    $_CRP = $worker.GetType().GetProperty('CurrentlyRunningPipeline',$Flag)
    $CRP = $_CRP.GetValue($Worker)
    $State = If ($list[$_].handle.IsCompleted -AND -NOT [bool]$CRP) {
        'Completed'
    } 
    ElseIf (-NOT $list[$_].handle.IsCompleted -AND [bool]$CRP) {
        'Running'
    }
    ElseIf (-NOT $list[$_].handle.IsCompleted -AND -NOT [bool]$CRP) {
        'NotStarted'
    }
    [pscustomobject]@{
        Id = (([int]$_)+1)
        HandleComplete = $list[$_].handle.IsCompleted
        PipelineRunning = [bool]$CRP
        State = $State
    }
}

Running this code will produce the following output which will show the states for each runspace:

image

Note that I haven’t yet added this to PoshRSJob as I need to ensure that I add the appropriate code within the Type file within the module so the correct results are shown each time you run Get-RSJob. I will be sure to update this blog and post out to Twitter when that has occurred. Note that if you use this approach that you will either have to use this each time you want to view the status of the runspace, or you create a custom object with its own specific type and then supply your own type file (or use Update-TypeData) to automatically perform the checks each time you look at the object.

This entry was posted in powershell and tagged , , , . Bookmark the permalink.

3 Responses to Tracking the State of PowerShell Runspaces in a Runspacepool

  1. Nick says:

    How close are you to releasing an updated poshrs version that includes showing the correct run status for each of the jobs?

  2. Pingback: New Updates in PoshRSJob 1.7.3.5 | Learn Powershell | Achieve More

  3. Jaap Brasser says:

    Love the stuff you are doing with runspaces Boe, keep the good stuff coming!

Leave a comment