2017-01-24 4 views
7

В настоящее время мы реорганизуем наши сценарии администрирования. Только что появилось, что комбинация WinRM, обработки ошибок и ScriptMethod значительно снижает доступную глубину рекурсии.Увеличение размера стека с помощью WinRM для ScriptMethods

Смотрите следующий пример:

Invoke-Command -ComputerName . -ScriptBlock { 
    $object = New-Object psobject 
    $object | Add-Member ScriptMethod foo { 
     param($depth) 
     if ($depth -eq 0) { 
      throw "error" 
     } 
     else { 
      $this.foo($depth - 1) 
     } 
    } 

    try { 
     $object.foo(5) # Works fine, the error gets caught 
    } catch { 
     Write-Host $_.Exception 
    } 

    try { 
     $object.foo(6) # Failure due to call stack overflow 
    } catch { 
     Write-Host $_.Exception 
    } 
} 

Просто шесть вложенных вызовов достаточно переполнение стека вызовов! Действительно, более 200 локальных вложенных вызовов работают нормально, и без try-catch доступная глубина удваивается. Регулярные функции также не ограничены в рекурсии.

Примечание: Я использовал рекурсию только для воспроизведения проблемы, настоящий код содержит множество различных функций для разных объектов в разных модулях. Таким образом, тривиальные оптимизации как «функции использования, а не ScriptMethod» требуют архитектурных изменений

Есть ли способ увеличить размер доступных стеков? (У меня есть учетная запись администратора.)

+1

'throw error' - что такое« ошибка »? –

+0

Если я использую что-то вроде 'throw 'MY ERROR", то я не могу воспроизвести проблему (по крайней мере, не так, как описано). –

+0

@RomanKuzmin этот код может быть выполнен как есть (если включен WinRM на локальном компьютере) –

ответ

0

У вас есть две проблемы, которые сговариваются, чтобы сделать это трудным. Ни один из них не будет эффективно решен путем увеличения размера стека, если это возможно (я не знаю, если это так).

Во-первых, как вы уже пережили, удаленное добавление накладных расходов вызывает вызовы, которые уменьшают доступный стек. Я не знаю почему, но это легко продемонстрировать, что это так. Это может быть связано с тем, как настраиваются рабочие пространства или как интерпретатор вызывается, или из-за увеличения объема бухгалтерии - я не знаю конечной причины (причин).

Во-вторых, гораздо более уныло, ваш метод производит кучу вложенные исключения, а не только один. Это происходит потому, что скриптовый метод, по сути, представляет собой блок сценария, завернутый в другой обработчик исключений, который вызывает это исключение как MethodInvocationException. В результате, когда вы звоните foo(N), блок вложенных обработчиков исключений устанавливаются (перефразировать, это на самом деле не PowerShell кода, который делает это):

try { 
    try { 
     ... 
     try { 
      throw "error" 
     } catch { 
      throw [System.Management.Automation.MethodInvocationException]::new(
       "Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""", 
       $_.Exception 
      ) 
     } 
     ... 
    } catch { 
     throw [System.Management.Automation.MethodInvocationException]::new(
      "Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""", 
      $_.Exception 
     ) 
    } 
} catch { 
    throw [System.Management.Automation.MethodInvocationException]::new(
     "Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""", 
     $_.Exception 
    ) 
} 

Это производит массивные трассировки стеки, что в конечном итоге перетекает все разумные границы , Когда вы используете удаленный доступ, проблема усугубляется тем фактом, что даже если скрипт выполняет и создает это огромное исключение, он (и любые результаты, которые выполняет функция) не может быть успешно удален - на моей машине, используя PowerShell 5, Я не получаю ошибку переполнения стека, но удаляет ошибку при вызове foo(10).

Решение здесь заключается в том, чтобы избежать этой конкретной смертельной комбинации методов и исключений рекурсивного сценария. Предполагая, что вы не хотите, чтобы избавиться от любой рекурсии или исключения, это легче всего сделать, обернув регулярную функцию:

$object = New-Object PSObject 
$object | Add-Member ScriptMethod foo { 
    param($depth) 

    function foo($depth) { 
     if ($depth -eq 0) { 
      throw "error" 
     } 
     else { 
      foo ($depth - 1) 
     } 
    } 
    foo $depth 
} 

В то время как это производит гораздо более приятные исключения, даже это может довольно быстро бежать из стека когда вы удаляетесь. На моей машине это работает до foo(200); кроме этого я получаю переполнение глубины вызова. Локально, предел намного выше, хотя PowerShell становится неоправданно медленным с большими аргументами.

В качестве языка сценариев PowerShell не был точно разработан для эффективной рекурсии. Если вам нужно больше, чем foo(200), моя рекомендация - укусить пулю и переписать ее, чтобы она не была рекурсивной.Классы, как Stack<T> может помочь здесь:

$object = New-Object PSObject 
$object | Add-Member ScriptMethod foo { 
    param($depth) 

    $stack = New-Object System.Collections.Generic.Stack[int] 
    $stack.Push($depth) 

    while ($stack.Count -gt 0) { 
     $item = $stack.Pop() 
     if ($item -eq 0) { 
      throw "error" 
     } else { 
      $stack.Push($item - 1) 
     } 
    } 
} 

Очевидно foo тривиальным хвостовая рекурсия, и это излишеством, но она иллюстрирует идею. Итерации могут вызывать более одного элемента в стеке.

Это не только устраняет проблемы с ограниченной глубиной стека, но и намного быстрее.

+0

Я использовал рекурсию только для демонстрации - в реальном коде есть 5 различных функций на разных объектах. –

+0

@PavelMayorov: вы имеете в виду взаимную рекурсию между различными сценариями? В таком случае, да, мой подход не поможет вам. В этом случае я полагаю, что вы Boned (tm), если переписать не вариант, если только кто-то не придумал какой-нибудь умный способ настроить размер стека. Обратите внимание, что проблема с обработчиками обработанных исключений остается актуальной, функциональной проблемой для таких скриптовых методов. Похоже, что ваша база кода использует скриптовые методы как способ сделать O-O в PowerShell, что является хорошей идеей в теории, но не на практике. –

+0

Нет. У меня нет рекурсии в реальном коде. –

0

Может быть стоит проверить это, если вы наката доступной памяти на удаленном сеансе: Running Java remotely using PowerShell

Я знаю, что для запуска приложения Java, но решение обновляет максимальную память, доступную для удаленного сеанса WinRM.

+0

У меня достаточно памяти для этого простого скрипта (1Gb). Корень проблемы - размер стека. –

+0

Это должен быть комментарий в OP, а не ответ. –

Смежные вопросы