1

I need some help with a async Powershell call that fails with either (1) max recursion depth exceeded or (2) invalid input argument.

The basic setup is as follows...

  • Coding and debugging done in VSCode
  • Powershell version is 5.1.19041 - desktop edition
  • Function to connect to SSAS server
  • Recursive function called by first function to convert object to XML
  • Create a Powershell session to execute the functions with parameters
  • BeginInvoke(), wait for completion, process results

Is there something with the async call that is messing with my code such that the comparison between $NestLevel and $MaxDepth is invalid or that $MaxDepth is set to 0?

I've gone over this a hundred times and I don't see anywhere $MaxDepth is ever set to anything below 1. The "if ( $NestLevel -gt $MaxDepth ) { return }" should terminate recursion but doesn't seem like it is.

Am I missing something?

Code example...

function Test-Recursion {
            Param (
                [Parameter(Mandatory=$false)]
                $InputObject,
        
                [Parameter(Mandatory=$false)]
                [ValidateRange(1,100)]
                [Int16] $MaxDepth   = 2
            )
        
            [Int16] $_NestLevel_ += 1
            if ( $_NestLevel_ -gt $MaxDepth ) { return }
        
            "<item><nest_level>$($_NestLevel_)</nest_level><max_depth>$($MaxDepth)</max_depth></item>"
            & $MyInvocation.MyCommand.ScriptBlock -InputObject $InputObject -MaxDepth $MaxDepth
        }
        
function Get-Info {
                Param (
                    [Parameter(Mandatory=$true)]
                    [String] $HostName,
        
                    [Parameter(Mandatory=$true)]
                    [ScriptBlock] $ConvertTo
                )
                
                $some_collection | `
                ForEach-Object {
                    @(
                        $_as  | ForEach-Object {
                            & $ConvertTo -InputObject $_ -MaxDepth 3 
                        }
                    ) | Sort-Object
                }
        }
        
        # this code fails with either...
        #   - excessive recursion depth
        #   - invalid $MaxDepth value of 0
        try {
            $_ps    = [powershell]::Create()
            $null   = $_ps.AddScript(${function:Get-Info})
            $null   = $_ps.AddParameter('HostName', 'some_server_name')
            $null   = $_ps.AddParameter('ConvertTo', ${function:Test-Recursion}) 
        
            $_handle    = $_ps.BeginInvoke()
            while ( -Not $_handle.IsCompleted ) {
                Start-Sleep -Seconds 5
            }
        
            $_result    = $_ps.EndInvoke($_handle)
            $_result
        } catch {
            $_
        } finally {
            $_ps.Dispose()
        }
        
        # this code works as expected
        $items = Get-Info -HostName 'some_server_name' -ConvertTo ${function:Test-Recursion}
        $items.table_data
2
  • You might be facing a "depth overflow", you should consider using a Queue instead of recursion, see a similar issue here stackoverflow.com/a/71366308/15339544 Commented Jun 2, 2022 at 16:38
  • @SantiagoSquarzon I know I'm dealing with "depth overflow". What I don't understand is why it only happens with an async call but works when called synchronously. I'll look into the Queue option you mentioned. Thanks. Commented Jun 2, 2022 at 16:53

1 Answer 1

1

I'm not going to go into the details of what you're looking to accomplish with these functions, will just point out what the issue is, your powershell instance is running out of stack memory due to an infinite loop caused by your Test-Recursion function not knowing when to stop, hence the addition of a new parameter ([Int16] $NestingLevel) that will be passed as argument on each recursive call:

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2,

        [Parameter(DontShow)]
        [Int16] $NestingLevel
    )

    [Int16] $NestingLevel += 1
    if ( $NestingLevel -gt $MaxDepth ) {
        return
    }

    "<item><nest_level>$NestingLevel</nest_level><max_depth>$MaxDepth</max_depth></item>"
    Test-Recursion -InputObject $InputObject -MaxDepth $MaxDepth -NestingLevel $NestingLevel
}

Test-Recursion -InputObject hello -MaxDepth 3

It is also worth noting that your recursive function can be simplified with a while loop:

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2,

        [Parameter(DontShow)]
        [Int16] $NestingLevel
    )

    while($NestingLevel++ -lt $MaxDepth) {
        "<item><nest_level>$NestingLevel</nest_level><max_depth>$MaxDepth</max_depth></item>"
    }
}

Regarding your latest comment:

The root call initializes the variable to 1. Recursive calls would increment the variable and effectively provide a way to check the current nesting level.

No, the variable lives on each call of the function and then the incremented value is lost after a recursive call. You can however initialize the variable in the $script: scope and then it would work:

[Int16] $script:_NestLevel_ += 1

On a personal note, I do not like having $script: scoped variables inside my functions, there are better ways to do it in my opinion.

An easy way to demonstrate this is by modifying the function to output $_NestLevel_ on each call, as-is, you would see an infinite amount of 1 being displayed to your console.

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2
    )

    [Int16] $_NestLevel_ += 1
    if ( $_NestLevel_ -gt $MaxDepth ) { return }
    $_NestLevel_
    & $MyInvocation.MyCommand.ScriptBlock -InputObject $InputObject -MaxDepth $MaxDepth
}
Sign up to request clarification or add additional context in comments.

4 Comments

I am under the impression the "[Int16] $NestingLevel += 1" would result in a function scoped variable. The root call initializes the variable to 1. Recursive calls would increment the variable and effectively provide a way to check the current nesting level. Am I misunderstanding how variable scoping works? I will try your recommendation. Thanks again.
@AlwaysLearning it would only work if you change the scope of that variable: [Int16] $script:_NestLevel_ += 1 as-is, the variable lives on each call of the function and then is lost on each call of the function
@AlwaysLearning hopefully the update on my answer helps you clarifying it
The script behavior has changed for the better. At least I'm making progress now. Thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.