Powershell и глубина стека
В первую очередь была проведена работа по унификации интерфейсов заданий и были выделены следующие методы:
$Task1_Config = ...;
# проверить, возможно ли выполнить шаг развертывания.
function Task1_CheckRequirements() {}
# проверить, необходимо ли выполнять шаг развертывания.
function Task1_CanExecute($project) {}
# выполнить шаг развертывания.
function Task1_Execute($project, $context) {}
Учитывая, что подобных шагов становилось все больше и больше, поддерживать в таком виде скрипты становилось все сложнее. Изучив возможные решения, было принято решение реализовать каждое задание как отдельный объект:
function Task1()
{
$result = New-Object -Typename PSObject -Property `
@{
"name" = "Task1"
"config" = ...
}
Add-Member -InputObject $result -MemberType ScriptMethod -Name CheckRequirements -Value `
{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name CanExecute -Value `
{
Param($project)
}
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($project, $context)
}
return $result
}
В таком виде скрипты развертывания работали продолжительное время и ничто не предвещало беды. В один прекрасный момент передо мной встала задача провести развертывание на удаленном сервере. В powershell есть очень удобный механизм WinRM, который мы ранее очень активно использовали, и, соответственно, для решения поставленной задачи остановился на нем-же.
Решение работало нестабильно. На некоторых заданиях развертывания возникали либо ошибка либо Invoke-Command показывал, что удаленный скрипт выполнился корректно, но по факту он прерывался.
Не удалось обработать данные удаленной команды. Сообщение об ошибке: Ведущий процесс поставщика WSMan не вернул правильный ответ. Поставщик в ведущем процессе может вести себя неправильно
Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response. A provider in the host process may have behaved improperly.
В EventViewer смог найти, что процесс на удаленной машине завершался с ошибкой 1726, но никакой вразумительной информации об ошибке обнаружить не удавалось. При этом запуск того-же самого задания на удаленной машине всегда завершалось успешно.
В ходе многочисленных экспериментов поймал в ошибку The script failed due to call depth overflow которая определила дальнейшее направление исследований.
Со времен PowerShell v2 максимальная глубина стека в скриптах powershell составляет 1000 вызовов, в последующих версиях это значение было еще существенно поднято и ошибок типа stack overflow никогда не возникало.
Решил провести несколько тестов для определения глубины стека при вызове локально и через WinRM. Для этого подготовил инструментарий тестирования.
$ErrorActionPreference = "Stop"
$cred = New-Object System.Management.Automation.PsCredential(...)
function runLocal($sb, $cnt)
{
Write-Host "Local $cnt"
Invoke-Command -ScriptBlock $sb -ArgumentList @($cnt)
}
function runRemote($sb, $cnt)
{
Write-Host "Remote $cnt"
$s = New-PSSession "." -credential $cred
try
{
Invoke-Command -Session $s -ScriptBlock $sb -ArgumentList @($cnt)
}
finally
{
Remove-PSSession -Session $s
}
}
Первый тест определял возможную глубину рекурсии:
$scriptBlock1 =
{
Param($cnt)
function test($cnt)
{
if($cnt -ne 0)
{
test $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
}
test $cnt
}
runLocal $scriptBlock1 3000
runRemote $scriptBlock1 150
runRemote $scriptBlock1 160
----------
Local 3000
Call depth: 3004
Remote 150
Call depth: 152
Remote 160
The script failed due to call depth overflow.
По результату — локально глубина стека более 3000, удаленно — немного больше 150.
150 — довольно большое значение. Достичь его в реальной работе скриптов развертывания нереально.
Второй тест определяет возможную глубину рекурсии при использовании объектов:
$scriptBlock2 =
{
Param($cnt)
function test()
{
$result = New-Object -Typename PSObject -Property @{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($cnt)
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
}
return $result
}
$obj = test
$obj.Execute($cnt)
}
runLocal $scriptBlock2 3000
runRemote $scriptBlock2 130
runRemote $scriptBlock2 135
----------
Local 3000
Call depth: 3004
Remote 130
Call depth: 132
Remote 135
Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response.
Результаты немного хуже. Удаленно глубина стека 130–133. Но для работы это тоже очень большое значение.
Дальнейшее изучение исходных скриптов развертывания натолкнуло на мысль проверить, как работают try-catch блоки:
$scriptBlock3 =
{
Param($cnt)
function test()
{
$result = New-Object -Typename PSObject -Property @{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($cnt)
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
return $result
}
try
{
$obj = test
$obj.Execute($cnt)
}
catch
{
Write-Host " Exception catched"
}
}
runLocal $scriptBlock3 130
runRemote $scriptBlock3 5
runRemote $scriptBlock3 6
----------
Local 130
Call depth: 134
Exception catched
Remote 5
Call depth: 7
Exception catched
Remote 6
Call depth: 8
The script failed due to call depth overflow.
И вот тут меня ожидал огромный сюрприз. При использовании «объектов» и генерации исключительной ситуации возможная глубина стека локально составила около 130, а удаленно всего 5.
$scriptBlock4 =
{
Param($cnt)
function test($cnt)
{
if($cnt -ne 0)
{
test $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
try
{
test $cnt
}
catch
{
Write-Host " Exception catched"
}
}
runLocal $scriptBlock4 2000
runRemote $scriptBlock4 150
----------
Local 2000
Call depth: 2004
Exception catched
Remote 150
Call depth: 152
Exception catched
Но при отказе от использования «объектов» проблема исчезала. Значения глубины стека оказались на уровне первого теста.
В powershell 5 появились классы. Провел тест с их использованием:
$scriptBlock5 =
{
Param($cnt)
Class test
{
Execute($cnt)
{
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
}
try
{
$t = [test]::new()
$t.Execute($cnt)
}
catch
{
Write-Host "Exception catched"
}
}
runLocal $scriptBlock5 130
runRemote $scriptBlock5 7
runRemote $scriptBlock5 8
----------
Local 130
Call depth: 134
Exception catched
Remote 7
Call depth: 9
Exception catched
Remote 8
Call depth: 10
The script failed due to call depth overflow.
Особого выигрыша не получили. При вызове через WinRM глубина стека составила всего 7 хопов. Чего так-же недостаточно для нормальной работы скриптов.
Работая со скриптами тестирования пришла мысль реализовать объекты при помощи hash + script block.
$scriptBlock6 =
{
Param($cnt)
function Call($self, $scriptName, [parameter(ValueFromRemainingArguments = $true)] $args)
{
$args2 = @($self) + $args
Invoke-Command -ScriptBlock $self.$scriptName -ArgumentList $args2
}
function test()
{
$result = @{ }
$result.Execute =
{
Param($self, $cnt)
if($cnt -ne 0)
{
Call $self Execute $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
return $result
}
try
{
$obj = test
Call $obj Execute $cnt
}
catch
{
Write-Host "Exception catched"
}
}
runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
----------
runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
Local 1000
Call depth: 2005
Exception catched
Remote 55
Call depth: 113
Exception catched
Remote 60
Exception catched
Глубина стека в 55 хопов — это уже вполне достаточное значение.
Ниже свел в одну таблицу результаты тестирования доступной глубина стека:
локально | через winRM | |
Функция | >3000 | ~150 |
Метода объекта | >3000 | ~130 |
Метода объекта с try-catch | ~130 | 5 |
Функция с try-catch | >2000 | ~150 |
Метода класса (PS5) с try-catch | ~130 | 7 |
Hash + script block с try-catch | >1000 | ~55 |
Надеюсь, что эта информация окажется полезной не только мне! :)