【问题标题】:PowerShell Try/Catch and RetryPowerShell 尝试/捕获和重试
【发布时间】:2018-01-10 06:41:43
【问题描述】:

我有一个相当大的 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"

有没有更好的方法来做到这一点?

【问题讨论】:

    标签: powershell error-handling try-catch


    【解决方案1】:

    如果您经常需要多次重试操作的代码,您可以将循环的 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 {
                    $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 尝试失败后终止脚本,可以使用另一个参数进行配置,这样您就可以拥有导致脚本在失败时停止的操作,以及可以忽略失败的操作。

    【讨论】:

    • 对此进行了测试,如果出现错误,它似乎与 $ScriptBlock.Invoke() 没有被捕获。使用 write-host 语句,我确认正在调用 retry-command,使其进入 do 循环,使其进入 try 循环,但我故意将数据输入到我的脚本块中,这会导致错误但没有捕获错误
    • 我的脚本块如下所示:Invoke-Sqlcmd -Query "DELETE FROM This_Table_Does_NOT_Exist" -ServerInstance mydbserver -Database TestDB。如果我从脚本外部运行命令,则会收到错误消息:Invoke-Sqlcmd : Invalid object name 'This_Table_Does_NOT_Exist'。
    • 错误似乎是non-terminating error。尝试将-ErrorAction Stop 添加到Invoke-Sqlcmd 语句中。
    • 我在发布之前测试了代码,变量可以很好地使用。不过,我的示例代码在 Write-Error 语句中缺少 -ErrorAction Continue(因此代码不会在第一个错误时终止)。
    • 我建议在写出异常后进行可配置的睡眠。
    【解决方案2】:

    我改编了@Victor 的回答并补充道:

    • 重试参数
    • ErrorAction 设置和恢复(否则异常不会被捕获)
    • 指数退避延迟(我知道 OP 没有要求这样做,但我使用它)
    • 摆脱了 VSCode 警告(即将 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
    

    【讨论】:

      【解决方案3】:

      将委托传递给函数而不是脚本块的解决方案:

      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!"})
      

      【讨论】:

        【解决方案4】:

        错误处理总是会为您的脚本添加更多内容,因为它通常必须处理许多不同的事情。如果您想让每个函数都有多次尝试,Try Catch 函数可能最适合您上面描述的内容。自定义函数甚至允许您通过每次传入一个值来设置尝试之间的睡眠计时器,或者改变该函数将尝试的尝试次数。

        【讨论】:

        • 您可能还想弄清楚为什么需要重试步骤 5 次。无论如何,拥有一个 trycatch 函数也是创建日志输出并可能对其进行排序的好地方......
        • 这主要是一种预防措施。所以我们试图涵盖一些错误场景:1)这是一个终端错误(我们应该退出)2)这是一个非终端错误,我们应该继续但警告错误3)这是一个临时的错误(重试会有帮助吗?)。一个例子可能是我们试图对尚未完全启动的 VM 或云实例运行操作。在这种情况下,等待并重试是一个不错的选择。虽然最终它应该放弃。
        猜你喜欢
        • 1970-01-01
        • 2019-04-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-22
        • 1970-01-01
        • 2017-09-12
        相关资源
        最近更新 更多