Dealing with Runspacepool Variable Scope Creep in PowerShell

Something that I had noticed a while back that I would receive some output values in my objects that shouldn’t have been there. Properties that should have been Null had values in them, and not just random values, but values that matched some of the other outputted objects. This was somewhat worrisome because I didn’t want my project, PoshRSJob, to have this same problem (which it did have). Duplicating the issue was simple, and actually fixing ended up being simple but it wasn’t something that immediately stood out to me, mostly because I had thought that I tested out the solution already and it didn’t work.

The solution to this also shows that sometimes the simplest of fixes can solve the more annoying or complex problems as well as showing that sometimes it is ok to take a little time away from an issue (as long as it isn’t critical to something) and coming back to it can help refresh your mind so you can come back more focused on the issue.

My demo code below will run a simple script block and outputs an object with a few properties that show the pipeline number (basically just something to supply as an outside variable into the scriptblock), the ThreadID so I know that the runspacepool is behaving by reusing the existing runspaces and lastly a boolean value that is probably the most important value in that it shows whether the pipeline value (the first item in our object) is either an odd or even number. If it isn’t an odd number, then it should be a null value and if it is odd, then a boolean value of $True is used.

I’m determining whether the value is odd or even by using –BAND which is a Bitwise AND statement.

1..10 | ForEach {
    [pscustomobject]@{
        Number = $_
        IsOdd = [bool]($_ -BAND 1)
    }
}

image

You can see that this accurately shows which values are odd and which values are even.

Now in my demo code I will use the same logic but just won’t show False if it is even. The idea is that there should be an alternating Null/True value for each returned object.

#region RunspacePool Demo
$Parameters = @{}
$RunspacePool = [runspacefactory]::CreateRunspacePool(
    [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
)
[void]$RunspacePool.SetMaxRunspaces(2)
$RunspacePool.Open()
$jobs = New-Object System.Collections.ArrayList
1..10 | ForEach {
    $Parameters.Pipeline = $_
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    [void]$PowerShell.AddScript({
        Param (
            $Pipeline
        )
        If ($Pipeline -BAND 1) {
            $Fail = $True
        }
        $ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
        [pscustomobject]@{
            Pipeline = $Pipeline
            Thread = $ThreadID
            IsOdd = $Fail
        }
        #Remove-Variable fail
    })
    [void]$PowerShell.AddParameters($Parameters)
    [void]$jobs.Add((
        [pscustomobject]@{
            PowerShell = $PowerShell
            Handle = $PowerShell.BeginInvoke()
        }
    ))
}
While ($jobs.handle.IsCompleted -eq $False) {
    Write-Host "." -NoNewline
    Start-Sleep -Milliseconds 100
}
## Wait until all jobs completed before running code below
$return = $jobs | ForEach {
    $_.powershell.EndInvoke($_.handle)
    $_.PowerShell.Dispose()
}
$jobs.clear()
$return
#endregion RunspacePool Demo

Running this code shows that this fails rather impressively by showing all but a single returned object as being Odd with the Pipeline property value.

image

Yea, this is not a good thing if you are looking for any sort of accuracy in your returned data. I will note that you can mitigate this by calling the Remove-Variable cmdlet against the $Fail variable at the end of your scriptblock, but this isn’t really something that I would expect everyone to do as a solution should really be available within the module itself or at least some way to avoid handling this within the scriptblock.

I looked at many possible options such as changing the runspace thread options thinking that it was a symptom of reusing the same runspace and thinking that it was “just the way it is” and that users would have to remember to remove the variables at the end of the scriptblock execution. Thankfully, that was not the solution that I was going to need to stick with. I ended up stepping away from this issue for a few months because I just couldn’t figure out what was going on and my attempts to fix it ended in failure. Rather than chase my own tail on this, I decided that I would refocus my efforts on something else and then re-engage this at a later date. By doing this, I was able to think better on what I should be looking at as well as testing my different ideas out.

So the big question is: what is your solution to this issue? Well, it comes down to setting a value of $True on the UseLocalScope property when calling the AddScript() method early into the runspace build.

SNAGHTML1e6ff5e1

By setting this value, now everything behaves as expected and we no longer have the oddness of values being where they shouldn’t be at.

#region RunspacePool Demo
$Parameters = @{}
$RunspacePool = [runspacefactory]::CreateRunspacePool(
    [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
)
[void]$RunspacePool.SetMaxRunspaces(2)
$RunspacePool.Open()
$jobs = New-Object System.Collections.ArrayList
1..10 | ForEach {
    $Parameters.Pipeline = $_
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    [void]$PowerShell.AddScript({
        Param (
            $Pipeline
        )
        If ($Pipeline -BAND 1) {
            $Fail = $True
        }
        $ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
        [pscustomobject]@{
            Pipeline = $Pipeline
            Thread = $ThreadID
            Fail = $Fail
        }
        #Remove-Variable fail
    }, $True) #Setting UseLocalScope to $True fixes scope creep with variables in RunspacePool
    [void]$PowerShell.AddParameters($Parameters)
    [void]$jobs.Add((
        [pscustomobject]@{
            PowerShell = $PowerShell
            Handle = $PowerShell.BeginInvoke()
        }
    ))
}
While ($jobs.handle.IsCompleted -eq $False) {
    Write-Host "." -NoNewline
    Start-Sleep -Milliseconds 100
}
## Wait until all jobs completed before running code below
$return = $jobs | ForEach {
    $_.powershell.EndInvoke($_.handle)
    $_.PowerShell.Dispose()
}
$jobs.clear()
$return
#endregion RunspacePool Demo

image

Success! The $Fail variable doesn’t try to sneak across to another runspace that is running to prevent an inaccurate display of what number is odd when it should be treated as an even number by remaining Null.

Update: If you declare a Global variable within the scriptblock ($Global:Fail=$True), it will be treated as though you never wanted to use the UseLocalScope and will persist throughout all of the subsequent runspaces that you run in your runspacepool. Just another reason why you should always avoid using the Global scope unless absolutely needed!

So in the end, what was a pretty serious issue in how the variables and their values were being preserved across runspacepools ended up being a rather simple fix once I came back from a little time away from troubleshooting.

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

6 Responses to Dealing with Runspacepool Variable Scope Creep in PowerShell

  1. Steve says:

    Thank you so much for this.
    I’ve been in a panic all week for my largest project to date.
    The script casts datatable rows to the scriptblock. Strange results were occurring.
    By the end I had all the variables calling their values to the pipeline and watched as the WRONG $Row jumped 4 jobs, landing where it did not belong.
    Cold Sweat!
    I implemented your fix and now everyone is behaving themselves.

    Muchos Gracias

  2. bukem says:

    Is this change implemented in PoshRSJob 1.7.4.0 release?

  3. brendan62269 says:

    I guess another way to look at would be, when have you provided an output object within .AddScrip() and not observed this behavior? Maybe I’m wrong.

  4. brendan62269 says:

    So, once the threadsafe hash IsOdd finds a $true condition, it wants to keep the value. Do you have any other examples where this is errant or are is this just with regards to PSJobs? I have to add that I have used PSJobs several times in the past to both learn how rs jobs work and in production code. I have never come across any case where your code provided something other than the expected output, ever.
    In your first rs example (the errant one), I would expect the variable to change only if the condition is true, that is, if you happen across an odd number. If ($_ -band 1) {} doesn’t have an else {} to change IsOdd, then I would not expect it to change as it is a common variable shared/read/writable by all threads in the pool. The first $false in your list is, of course, the number 2 because the jobs spawned and processed their pipeline object at the same time (perhaps a mutex would change your output to ALL $True). As a matter of fact, I would consider it errant behavior if it were to change (acted like a locally scope variable, as you suggest the fix to be). And (obviously) putting an else {$Fail = $False} in your first rs example above provides the output you say you were expecting.
    Say you are spawning a pool of threads to look for something more insidious than a binary even/odd condition, maybe a needle in a haystack. I’d want the code to consistently report $false until it found the needle and not change to true until then. At that point all of my runspaces could quit looking (or whatever) when they catch the changed condition. In my honest opinion, this was not a problem or anything you needed to fix. The threadsafe hashtable was doing precisely what it’s supposed to.

Leave a comment