Skip to content

Instantly share code, notes, and snippets.

@deadlydog
Last active November 4, 2024 07:43
Show Gist options
  • Save deadlydog/620808036d309c8fa2606f32e5ef2f42 to your computer and use it in GitHub Desktop.
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
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
# ========== 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