Last active
November 4, 2024 07:43
-
-
Save deadlydog/620808036d309c8fa2606f32e5ef2f42 to your computer and use it in GitHub Desktop.
Powershell Invoke-ScriptBlockWithRetries function to execute arbitrary PowerShell code and automatically retry if an error occurs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
Export-ModuleMember -Function Invoke-ScriptBlockWithRetries |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ========== Examples of how to use module from another ps1 file ========== | |
using module .\PowerShellUtilities.psm1 | |
# === Example 1 - Action that does not return data === | |
[scriptblock] $exampleThatDoesNotReturnData = { | |
Stop-Service -Name "SomeService" | |
} | |
Invoke-ScriptBlockWithRetries -ScriptBlock $exampleThatDoesNotReturnData -ErrorsToNotRetry 'Cannot find any service with service name' | |
# === Example 2 - Capturing data returned === | |
[scriptblock] $exampleThatReturnsData = { | |
Invoke-WebRequest -Uri 'https://google.com' | |
} | |
$result = Invoke-ScriptBlockWithRetries -ScriptBlock $exampleThatReturnsData -MaxNumberOfAttempts 3 | |
if ($result.StatusCode -eq 200) { | |
Write-Output "Success" | |
} | |
# === Example 3 - Dealing with failures that still occur even with retries === | |
[string] $nonExistentWebAddress = 'https://SomeAddressThatDoesNotExist.com' | |
[scriptblock] $exampleThatWillAlwaysFail = { | |
Invoke-WebRequest -Uri $nonExistentWebAddress | |
} | |
try { | |
Invoke-ScriptBlockWithRetries -ScriptBlock $exampleThatWillAlwaysFail -MillisecondsToWaitBetweenAttempts 100 | |
} catch { | |
$exceptionMessage = $_.Exception.Message | |
Write-Error "An error occurred calling '$nonExistentWebAddress': $exceptionMessage" | |
# throw | |
} | |
# === Example 4 - Specify messages that should not be retried if the exception contains them === | |
[scriptblock] $exampleThatReturnsData = { | |
Invoke-RestMethod -Uri 'https://api.google.com' | |
} | |
[string[]] $noRetryMessages = @( | |
'400 (Bad Request)' | |
'401 (Unauthorized)' | |
'404 (Not Found)' | |
) | |
Invoke-ScriptBlockWithRetries -ScriptBlock $exampleThatReturnsData -ErrorsToNotRetry $noRetryMessages | |
# === Example 5 - Random results from flaky function === | |
[scriptblock] $flakyAction = { | |
$random = Get-Random -Minimum 0 -Maximum 10 | |
if ($random -lt 2) { | |
Write-Output "Success" | |
} elseif ($random -lt 4) { | |
Write-Error "Error" | |
} elseif ($random -lt 6) { | |
Write-Error "Error DoNotRetry" | |
} elseif ($random -lt 8) { | |
throw "Exception" | |
} else { | |
throw "Exception DoNotRetry" | |
} | |
} | |
Invoke-ScriptBlockWithRetries -ScriptBlock $flakyAction -MaxNumberOfAttempts 10 -MillisecondsToWaitBetweenAttempts 100 -ExponentialBackoff -ErrorsToNotRetry "DoNotRetry" -Verbose |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment