Private/Misc/Invoke-WithRetry.ps1
function Invoke-WithRetry { <# .SYNOPSIS Executes a script block with retry logic for transient failures. .DESCRIPTION This function provides retry logic for operations that may fail due to transient issues such as network timeouts, temporary server errors, or rate limiting. .PARAMETER ScriptBlock The script block to execute with retry logic. .PARAMETER RetryCount The maximum number of retry attempts. Default is 3. .PARAMETER RetryDelay The base delay in seconds between retry attempts. Default is 1 second. The actual delay will use exponential backoff with jitter. .PARAMETER RetryableExceptions Array of exception types or HTTP status codes that should trigger a retry. Defaults to common transient error conditions. .PARAMETER MaxRetryDelay The maximum delay in seconds between retries. Default is 30 seconds. .PARAMETER UseExponentialBackoff Whether to use exponential backoff for retry delays. Default is $true. .PARAMETER UseJitter Whether to add random jitter to retry delays to avoid thundering herd. Default is $true. .EXAMPLE $result = Invoke-WithRetry -ScriptBlock { Invoke-WebRequest -Uri 'https://api.example.com/data' } .EXAMPLE $result = Invoke-WithRetry -ScriptBlock { Get-SomeData } -RetryCount 5 -RetryDelay 2 #> [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock] $ScriptBlock, [ValidateRange(0, 10)] [int] $RetryCount = 3, [ValidateRange(0.1, 300)] [double] $RetryDelay = 1.0, [string[]] $RetryableExceptions = @( 'System.Net.WebException', 'System.TimeoutException', 'System.Net.Http.HttpRequestException', 'Microsoft.PowerShell.Commands.HttpResponseException' ), [int[]] $RetryableStatusCodes = @( 408, # Request Timeout 429, # Too Many Requests 500, # Internal Server Error 502, # Bad Gateway 503, # Service Unavailable 504 # Gateway Timeout ), [ValidateRange(1, 300)] [double] $MaxRetryDelay = 30.0, [bool] $UseExponentialBackoff = $true, [bool] $UseJitter = $true ) $attempt = 0 $lastException = $null while ($attempt -le $RetryCount) { try { Write-Verbose "Executing attempt $($attempt + 1) of $($RetryCount + 1)" # Execute the script block $result = & $ScriptBlock # If we get here, the operation succeeded if ($attempt -gt 0) { Write-Verbose "Operation succeeded on attempt $($attempt + 1)" } return $result } catch { $lastException = $_ $attempt++ # Check if we should retry this exception $shouldRetry = $false # Check HTTP status codes for web exceptions first $statusCode = $null if ($_.Exception -is [System.Net.WebException]) { if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } } elseif ($_.Exception.GetType().FullName -eq 'Microsoft.PowerShell.Commands.HttpResponseException') { # Use type name comparison for PowerShell version compatibility $statusCode = [int]$_.Exception.Response.StatusCode } elseif ($_.TargetObject -and $_.TargetObject.StatusCode) { # For mock objects in tests $statusCode = [int]$_.TargetObject.StatusCode } # For HTTP exceptions, only retry if the status code is in the retryable list if ($statusCode) { if ($statusCode -in $RetryableStatusCodes) { $shouldRetry = $true Write-Verbose "Retryable HTTP status code detected: $statusCode" } else { Write-Verbose "Non-retryable HTTP status code detected: $statusCode" } } else { # For non-HTTP exceptions, check exception type $exceptionType = $_.Exception.GetType().FullName if ($exceptionType -in $RetryableExceptions) { $shouldRetry = $true Write-Verbose "Retryable exception detected: $exceptionType" } } # If this is our last attempt or the error is not retryable, throw if ($attempt -gt $RetryCount -or -not $shouldRetry) { if (-not $shouldRetry) { Write-Verbose "Non-retryable error encountered: $exceptionType $(if ($statusCode) { "HTTP $statusCode" })" } else { Write-Verbose "Maximum retry attempts ($RetryCount) exceeded" } throw $lastException } # Calculate delay for next attempt $delay = $RetryDelay if ($UseExponentialBackoff) { # Exponential backoff: delay = base * (2 ^ attempt) $delay = $RetryDelay * [Math]::Pow(2, $attempt - 1) } # Apply jitter (random ±25% variation) if ($UseJitter) { $jitterRange = $delay * 0.25 $jitter = (Get-Random -Minimum (-$jitterRange) -Maximum $jitterRange) $delay += $jitter } # Cap the delay at maximum $delay = [Math]::Min($delay, $MaxRetryDelay) Write-Verbose "Retrying in $([Math]::Round($delay, 2)) seconds (attempt $attempt of $RetryCount)" Write-Warning "Operation failed, retrying in $([Math]::Round($delay, 2)) seconds. Error: $($_.Exception.Message)" # Wait before retrying Start-Sleep -Seconds $delay } } # This should never be reached, but just in case throw $lastException } |