ps/Modules/Alkami.PowerShell.Common/Public/Invoke-CommandWithRetry.ps1

114 lines
4.4 KiB
PowerShell
Raw Normal View History

2023-05-30 22:51:22 -07:00
function Invoke-CommandWithRetry {
<#
.SYNOPSIS
Executes a script block and retries the operation a given number of times if it fails.
Executions are applied by either linear or exponential format and always contain an amount of random jitter in milliseconds.
The jitter range can be managed by appropriate flags. Defaults are 50ms to 500ms.
.EXAMPLE
$result = Invoke-CommandWithRetry -ScriptBlock $script -MaxRetries 3 -SecondsDelay 1 -Exponential
.PARAMETER ScriptBlock
The script block to execute
.PARAMETER Arguments
An array of objects to pass into the scriptblock
.PARAMETER MaxRetries
The maxumim number of retries to attempt. Defaults to 5.
.PARAMETER Seconds
The number of seconds to delay for each retry. Defaults to 1 when used.
.PARAMETER Milliseconds
The number of milliseconds to delay for each retry. Defaults to 1000 ms.
.PARAMETER Exponential
[switch] Exponentially increment the time delay by a power of 2 for each attempt
The alternative to exponential is linear.
.PARAMETER JitterMin
[int] The min number of milliseconds to jitter by
.PARAMETER JitterMax
[int] The max number of milliseconds to jitter by
#>
[CmdletBinding(DefaultParameterSetName='Milliseconds')]
[OutputType([System.Object])]
Param(
[Parameter(Mandatory=$true)]
[scriptblock]$ScriptBlock,
[Parameter(Mandatory=$false)]
[object[]]$Arguments = $null,
[Parameter(Mandatory=$false)]
[int]$MaxRetries = 5,
[Parameter(ParameterSetName='Seconds', Mandatory=$false)]
[Alias('SecondsDelay')]
[int]$Seconds = 1,
[Parameter(ParameterSetName='Milliseconds', Mandatory=$false)]
[int]$Milliseconds = 1000,
[Parameter(Mandatory=$false)]
[switch]$Exponential,
[Parameter(Mandatory=$false)]
[int]$JitterMin = -500,
[Parameter(Mandatory=$false)]
[int]$JitterMax = 500
)
$logLead = (Get-LogLeadName)
$retryCount = 0
if ($PSCmdlet.ParameterSetName -eq 'Seconds') {
$Milliseconds = $Seconds * 1000
}
do {
# Not being picked up by above call
$logLead = "[Invoke-CommandWithRetry]"
$retryCount++
try {
$ScriptBlock.Invoke($Arguments)
return
} catch {
Write-Warning $_.Exception.InnerException.Message
# Don't stall at the end of the failed run otherwise we delay error reporting by one more sleep cycle
if ($retryCount -lt $MaxRetries) {
# As a deconstructed equation because the below block covers a lot of ground
# $sleepTimeout = ($milliseconds * (1 OR a power of 2) ) + random jitter amount
# 1 -shl 0 = 1 1 -shl 3 = 8 3rd retry = 8 multiplier
# $sleepTimeout = ($milliseconds * (1 -shl (0,($retryCount - 1))[$Exponential])) + (Get-Random -Maximum $JitterMax -Minimum $JitterMin)
# $sleepTimeoutModifier
# -shiftleft
# $retryCount - 1 when exponential, 1 when linear
# $sleepTimeout = ($milliseconds * $sleepTimeoutModifier ) + $jitterAmount
$jitterAmount = Get-Random -Maximum $JitterMax -Minimum $JitterMin
$sleepTimeoutModifier = 1
if ($Exponential) {
# When you want 2 to the power, you just shift the binary leader left
# 1 = 0001
# 2 = 0010
# 4 = 0100
# 8 = 1000
$sleepTimeoutModifier = $sleepTimeoutModifier -shl ($retryCount - 1) #decremented because we start at 1 retry count
}
# 3rd retry would be base timeout * 8 + jitter
$sleepTimeout = ($milliseconds * $sleepTimeoutModifier) + $jitterAmount
Write-Warning "$logLead : Attempt #$RetryCount failed. Start-Sleep -Milliseconds [$sleepTimeout] for retry"
Start-Sleep -Milliseconds $sleepTimeout
} else {
Write-Warning "$logLead : Attempt #$RetryCount failed."
}
}
} while ($retryCount -lt $MaxRetries)
Write-Error "$logLead : Execution failed. Maximum retries attempted."
}