我有一个相当大的 powershell 脚本,其中包含许多(20 多个)执行各种操作的函数。
现在所有代码实际上都没有任何错误处理或重试功能。如果某个特定的任务/功能失败,它就会失败并继续。
我想改进错误处理并实现重试以使其更加健壮。
我在想类似的事情:
$tries = 0
while ($tries -lt 5) {
try{
# Do Something
# No retries necessary
$tries = 5;
} catch {
# Report the error
# Other error handling
}
}
问题是我需要执行很多步骤。
我认为上面的代码执行20次是没有意义的。这看起来真的是多余的。
我正在考虑编写一个带有单个参数的“TryCatch”函数,其中包含我想要调用的实际函数?
我也不确定这是否是正确的方法。我最终会不会得到一个类似以下内容的脚本:
TryCatch "Function1 Parameter1 Parameter2"
TryCatch "Function2 Parameter1 Parameter2"
TryCatch "Function3 Parameter1 Parameter2"
有更好的方法吗?
如果您经常需要多次重试某个操作的代码,您可以将循环的
try..catch
包装在函数中,并在脚本块中传递命令:
function Retry-Command {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true)]
[scriptblock]$ScriptBlock,
[Parameter(Position=1, Mandatory=$false)]
[int]$Maximum = 5,
[Parameter(Position=2, Mandatory=$false)]
[int]$Delay = 100
)
Begin {
$cnt = 0
}
Process {
do {
$cnt++
try {
# If you want messages from the ScriptBlock
# Invoke-Command -Command $ScriptBlock
# Otherwise use this command which won't display underlying script messages
$ScriptBlock.Invoke()
return
} catch {
Write-Error $_.Exception.InnerException.Message -ErrorAction Continue
Start-Sleep -Milliseconds $Delay
}
} while ($cnt -lt $Maximum)
# Throw an error after $Maximum unsuccessful invocations. Doesn't need
# a condition, since the function returns upon successful invocation.
throw 'Execution failed.'
}
}
像这样调用函数(默认重试 5 次):
Retry-Command -ScriptBlock {
# do something
}
或者像这样(如果在某些情况下需要不同数量的重试):
Retry-Command -ScriptBlock {
# do something
} -Maximum 10
功能还可以进一步完善,例如:通过使用另一个参数配置在
$Maximum
失败尝试后终止脚本,这样您就可以拥有在失败时导致脚本停止的操作,以及可以忽略失败的操作。
我改编了@Victor的答案并添加:
sleep
替换为 Start-Sleep
)# [Solution with passing a delegate into a function instead of script block](https://stackoverflow.com/a/47712807/)
function Retry()
{
param(
[Parameter(Mandatory=$true)][Action]$action,
[Parameter(Mandatory=$false)][int]$maxAttempts = 3
)
$attempts=1
$ErrorActionPreferenceToRestore = $ErrorActionPreference
$ErrorActionPreference = "Stop"
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
# exponential backoff delay
$attempts++
if ($attempts -le $maxAttempts) {
$retryDelaySeconds = [math]::Pow(2, $attempts)
$retryDelaySeconds = $retryDelaySeconds - 1 # Exponential Backoff Max == (2^n)-1
Write-Host("Action failed. Waiting " + $retryDelaySeconds + " seconds before attempt " + $attempts + " of " + $maxAttempts + ".")
Start-Sleep $retryDelaySeconds
}
else {
$ErrorActionPreference = $ErrorActionPreferenceToRestore
Write-Error $_.Exception.Message
}
} while ($attempts -le $maxAttempts)
$ErrorActionPreference = $ErrorActionPreferenceToRestore
}
# function MyFunction($inputArg)
# {
# Throw $inputArg
# }
# #Example of a call:
# Retry({MyFunction "Oh no! It happened again!"})
# Retry {MyFunction "Oh no! It happened again!"} -maxAttempts 10
将委托传递给函数而不是脚本块的解决方案:
function Retry([Action]$action)
{
$attempts=3
$sleepInSeconds=5
do
{
try
{
$action.Invoke();
break;
}
catch [Exception]
{
Write-Host $_.Exception.Message
}
$attempts--
if ($attempts -gt 0) { sleep $sleepInSeconds }
} while ($attempts -gt 0)
}
function MyFunction($inputArg)
{
Throw $inputArg
}
#Example of a call:
Retry({MyFunction "Oh no! It happend again!"})
错误处理总是会给你的脚本添加更多内容,因为它通常必须处理许多不同的事情。如果您想让每个函数都有多次尝试,那么 Try Catch 函数可能最适合您上面描述的内容。自定义函数甚至允许您通过每次传递一个值来设置尝试之间的睡眠计时器之类的东西,或者改变函数将尝试的尝试次数。
我出于同样的目的创建了以下函数。我在很多脚本中使用它。它允许重试终止错误(即抛出异常)和非终止错误(即写入错误),提供可选的指数退避,并允许不重试指定类型的错误(例如,没有必要尝试重试
Stop-Service -Name AServiceThatDoesNotExist
)。
function Invoke-ScriptBlockWithRetries {
[CmdletBinding(DefaultParameterSetName = 'RetryNonTerminatingErrors')]
param (
[Parameter(Mandatory = $true, HelpMessage = "The script block to execute.")]
[ValidateNotNull()]
[scriptblock] $ScriptBlock,
[Parameter(Mandatory = $false, HelpMessage = "The maximum number of times to attempt the script block when it returns an error.")]
[ValidateRange(1, [int]::MaxValue)]
[int] $MaxNumberOfAttempts = 5,
[Parameter(Mandatory = $false, HelpMessage = "The number of milliseconds to wait between retry attempts.")]
[ValidateRange(1, [int]::MaxValue)]
[int] $MillisecondsToWaitBetweenAttempts = 3000,
[Parameter(Mandatory = $false, HelpMessage = "If true, the number of milliseconds to wait between retry attempts will be multiplied by the number of attempts.")]
[switch] $ExponentialBackoff = $false,
[Parameter(Mandatory = $false, HelpMessage = "List of error messages that should not be retried. If the error message contains one of these strings, the script block will not be retried.")]
[ValidateNotNull()]
[string[]] $ErrorsToNotRetry = @(),
[Parameter(Mandatory = $false, ParameterSetName = 'IgnoreNonTerminatingErrors', HelpMessage = "If true, only terminating errors (e.g. thrown exceptions) will cause the script block will be retried. By default, non-terminating errors will also trigger the script block to be retried.")]
[switch] $DoNotRetryNonTerminatingErrors = $false,
[Parameter(Mandatory = $false, ParameterSetName = 'RetryNonTerminatingErrors', HelpMessage = "If true, any non-terminating errors that occur on the final retry attempt will not be thrown as a terminating error.")]
[switch] $DoNotThrowNonTerminatingErrors = $false
)
[int] $numberOfAttempts = 0
while ($true) {
try {
Invoke-Command -ScriptBlock $ScriptBlock -ErrorVariable nonTerminatingErrors
if ($nonTerminatingErrors -and (-not $DoNotRetryNonTerminatingErrors)) {
throw $nonTerminatingErrors
}
break # Break out of the while-loop since the command succeeded.
} catch {
[bool] $shouldRetry = $true
$numberOfAttempts++
[string] $errorMessage = $_.Exception.ToString()
[string] $errorDetails = $_.ErrorDetails
Write-Verbose "Attempt number '$numberOfAttempts' of '$MaxNumberOfAttempts' failed.`nError: $errorMessage `nErrorDetails: $errorDetails"
if ($numberOfAttempts -ge $MaxNumberOfAttempts) {
$shouldRetry = $false
}
if ($shouldRetry) {
# If the errorMessage contains one of the errors that should not be retried, then do not retry.
foreach ($errorToNotRetry in $ErrorsToNotRetry) {
if ($errorMessage -like "*$errorToNotRetry*" -or $errorDetails -like "*$errorToNotRetry*") {
Write-Verbose "The string '$errorToNotRetry' was found in the error message, so not retrying."
$shouldRetry = $false
break # Break out of the foreach-loop since we found a match.
}
}
}
if (-not $shouldRetry) {
[bool] $isNonTerminatingError = $_.TargetObject -is [System.Collections.ArrayList]
if ($isNonTerminatingError -and $DoNotThrowNonTerminatingErrors) {
break # Just break out of the while-loop since the error was already written to the error stream.
} else {
throw # Throw the error so it's obvious one occurred.
}
}
[int] $millisecondsToWait = $MillisecondsToWaitBetweenAttempts
if ($ExponentialBackoff) {
$millisecondsToWait = $MillisecondsToWaitBetweenAttempts * $numberOfAttempts
}
Write-Verbose "Waiting '$millisecondsToWait' milliseconds before next attempt."
Start-Sleep -Milliseconds $millisecondsToWait
}
}
}