Private/New-GitHubWriteRetryStrategy.ps1
|
<# .NOTES Dot-sourced by Infrastructure.GitHub.psm1. The consumed surface is New-GitHubWriteRetryStrategy; Test-GitHubConnectFailure is a file-private helper kept alongside the factory so the classification policy lives next to its sole consumer - mirroring the layout of Common.PowerShell's New-TransientNetworkRetryStrategy. Both are private to the module. If a second caller ever needs this classification the pair should move to Common.PowerShell's TransientErrorStrategies folder rather than being copied. #> # --------------------------------------------------------------------------- # Test-GitHubConnectFailure (private) # Decides whether a failure happened BEFORE the request could reach GitHub. # That is a stronger claim than "transient", and it is the only class of # failure a write can retry without risking a second execution server-side. # # The evidence is a System.Net.Sockets.SocketException somewhere in the # exception chain (HttpClient wraps it in HttpRequestException) whose # SocketErrorCode is a name-resolution or connect-establishment error. # Either way no request bytes ever left the machine, so a replay cannot # duplicate work GitHub has already done. # # Deliberately NARROWER than Common.PowerShell's transient-network policy, # which also matches timeouts, mid-flight connection resets and 5xx # responses. Each of those can mean GitHub received and acted on the # request before the response was lost - safe to replay for a read, # not for a write. # --------------------------------------------------------------------------- function Test-GitHubConnectFailure { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) # Socket errors raised while resolving the name or establishing the # connection. HostNotFound is the DNS case ("No such host is known"); # the rest are the connect attempt failing outright. # # TimedOut and ConnectionReset are deliberately absent: both can fire # after the request was written to the wire, so the server may have # processed it. $preRequestSocketErrors = @( [System.Net.Sockets.SocketError]::HostNotFound, [System.Net.Sockets.SocketError]::TryAgain, [System.Net.Sockets.SocketError]::NoData, [System.Net.Sockets.SocketError]::ConnectionRefused, [System.Net.Sockets.SocketError]::NetworkUnreachable, [System.Net.Sockets.SocketError]::HostUnreachable, [System.Net.Sockets.SocketError]::NetworkDown ) $ex = $ErrorRecord.Exception while ($null -ne $ex) { if ($ex -is [System.Net.Sockets.SocketException]) { return ($preRequestSocketErrors -contains $ex.SocketErrorCode) } $ex = $ex.InnerException } return $false } function New-GitHubWriteRetryStrategy { <# .SYNOPSIS Builds a retry strategy hashtable that matches only failures which provably never reached GitHub, for use on non-idempotent calls. .DESCRIPTION Returned shape is the standard retry-strategy contract consumed by Invoke-WithRetry: @{ Name = 'GitHubConnectFailure' ShouldRetry = { param($err) <bool> } } Matches DNS resolution failures and failed connect attempts, and nothing else. Timeouts, mid-flight resets and 5xx responses are classified as permanent here even though they are genuinely transient, because a POST/PATCH/DELETE replayed after one of them can double-execute. .EXAMPLE Invoke-WithRetry ` -ScriptBlock { Invoke-RestMethod @params } ` -RetryStrategy (New-GitHubWriteRetryStrategy) #> [CmdletBinding()] [OutputType([hashtable])] param() return @{ Name = 'GitHubConnectFailure' ShouldRetry = { param([System.Management.Automation.ErrorRecord] $ErrorRecord) Test-GitHubConnectFailure -ErrorRecord $ErrorRecord } } } |