functions/Invoke-RetryPSCommand.ps1
<#
.SYNOPSIS Invokes the provided PowerShell expression within a retry loop. .DESCRIPTION The Invoke-RetryPSCommand function evaluates or runs a specified [string] or [scriptblock] as a command and returns the results of the expression or command. This function should be used to improve reliability of commands used in situations where intermittent failures are expected from downstream systems. Expressions are evaluated and run in the current scope. To simplify use, default values are provided for the -RetryMultiplier (2) and -MaxRetry (5) parameters. These values can also be specified at invocation. To provide additional control over the retry logic, this function can operate in 3 modes: - Fixed (default) : Retry using a fixed time period, as specified by -RetryMultiplier (Alias = RetryDelay) - Linear : Retry using a linear backoff, calculated as the multiple of -RetryMultiplier and the -RetryCount value - Exponential : Retry using an exponential backoff, calculated as the RetryCount value to the power of the -RetryMultiplier value This function also support specifying a list of known errors which can be used to stop the retry loop if found. These can be used to either generate a warning (-ContinueOnErrors) or throw an error (-StopOnErrors) depending on the desired outcome. .EXAMPLE Invoke-RetryPSCommand -Command { Invoke-RestMethod -Uri "https://www.microsoft.com" } Run command with default settings .EXAMPLE Invoke-RetryPSCommand -Command { Invoke-RestMethod -Uri "https://www.microsoft.com" } -RetryDelay 5 -Linear Run command with linear backoff and custom RetryDelay .EXAMPLE Invoke-RetryPSCommand -Command { Invoke-RestMethod -Uri "https://www.microsoft.com" } -Exponential Run command with exponential backoff and default settings .EXAMPLE Invoke-RetryPSCommand -Command { Invoke-RestMethod -Uri "https://www.microsoft.com" } -ContinueOnErrors "404", "500" Run command with default settings, fail fast and Write-Warning for known errors .EXAMPLE Invoke-RetryPSCommand -Command { Invoke-RestMethod -Uri "https://www.microsoft.com" } -StopOnErrors "404", "500" Run command with default settings, fail fast and throw error for known errors .INPUTS None .OUTPUTS Returns the result of the specified Command #> function Invoke-RetryPSCommand { # The following SuppressMessageAttribute entries are used to surpress # PSScriptAnalyzer tests against known exceptions as per: # https://github.com/powershell/psscriptanalyzer#suppressing-rules [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification='The whole point of this function is to run a command using Invoke-Expression')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline, Position = 0, ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ValueFromPipeline, Position = 0, ParameterSetName = 'Linear')] [Parameter(Mandatory = $true, ValueFromPipeline, Position = 0, ParameterSetName = 'Exponential')] [Alias("C")] [scriptblock]$Command, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] [ValidateSet('Continue', 'Stop', 'SilentlyContinue')] [string]$FinalAction = 'Continue', [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] [string[]]$ContinueOnErrors, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] [string[]]$StopOnErrors, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] [Alias("RetryDelay")] [int]$RetryMultiplier = 2, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] [int]$MaxRetry = 5, [Parameter(Mandatory = $true, ParameterSetName = 'Linear')] [switch]$Linear, [Parameter(Mandatory = $true, ParameterSetName = 'Exponential')] [switch]$Exponential, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Linear')] [Parameter(Mandatory = $false, ParameterSetName = 'Exponential')] $ArgumentList ) begin { [regex]$regex_RemoveErrorHandling = "(?i)(-Error(Action|Variable)) ('|`")?\w+('|`")?" # Need to set ModeMessage and ModeParam depending on operating mode if ($Exponential) { $ModeParam = @{ Exponential = $true } } elseif ($Linear) { $ModeParam = @{ Linear = $true } } else { $ModeParam = @{} } } process { $matches = $null $RetryCount = 0 $EndLoop = $false $Test_Command_Error_Handling = $Command -match $regex_RemoveErrorHandling if ($Test_Command_Error_Handling) { Write-Warning "Found error handling in command. Removing value(s) '$($matches[0])' to ensure correct operation of retry loop." $Command = [Scriptblock]::Create($($regex_RemoveErrorHandling.Replace($Command, ''))) } do { try { $RetryCount++ $Output = Invoke-Command -ScriptBlock $Command -ArgumentList $ArgumentList -ErrorAction Stop $EndLoop = $true } catch { # Throw if the error message matches one provided in the parameter StopOnErrors if ($_ -in $StopOnErrors) { throw } # Skip retry if the error message matches one provided in the parameter ContinueOnErrors elseif ($_ -in $ContinueOnErrors) { Write-Warning $_ $EndLoop = $true } # Retry command if within MaxRetry threshold elseif ($RetryCount -le $MaxRetry) { Wait-RetryPSDelay $RetryMultiplier $RetryCount -Message "Caught Error: $_." -Warning @ModeParam } # Once all retries have completed, throw error else { $EndLoop = $true if ( $FinalAction -eq 'Continue' ) { Write-Warning $_ } elseif ($FinalAction -eq 'Stop') { throw } } } } until ($EndLoop) if ($Output) { return $Output } } } |