modules/shared/RateLimit.ps1
|
#Requires -Version 7.4 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function New-ProviderThrottleState { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateSet('Graph', 'ADO', 'GitHub', 'Azure')] [string] $Provider, [int] $ConcurrencyLimit ) $defaults = @{ Azure = 8 Graph = 4 ADO = 2 GitHub = 1 } if (-not $ConcurrencyLimit -or $ConcurrencyLimit -le 0) { $ConcurrencyLimit = $defaults[$Provider] } return [PSCustomObject]@{ Provider = $Provider ConcurrencyLimit = $ConcurrencyLimit ActiveRequests = 0 RetryAfterUntil = [datetime]::MinValue RemainingQuota = $null InitialQuota = $null ConsecutiveFailures = 0 CircuitBreakerThreshold = 5 CircuitOpenUntil = [datetime]::MinValue RemainingQuotaByHeader = @{} } } function Test-ShouldThrottle { [CmdletBinding()] param ( [Parameter(Mandatory)] [pscustomobject] $State ) $now = Get-Date $delay = 0.0 $reason = '' if ($State.CircuitOpenUntil -gt $now) { $delay = ($State.CircuitOpenUntil - $now).TotalSeconds $reason = 'CircuitOpen' } elseif ($State.RetryAfterUntil -gt $now) { $delay = ($State.RetryAfterUntil - $now).TotalSeconds $reason = 'RetryAfter' } elseif ($State.InitialQuota -and $State.RemainingQuota -ne $null) { $ratio = $State.RemainingQuota / [double]$State.InitialQuota if ($ratio -lt 0.1) { $delay = 5 $reason = 'LowQuota' } } if (-not $reason -and $State.ActiveRequests -ge $State.ConcurrencyLimit) { $delay = 1 $reason = 'ConcurrencyLimit' } return [PSCustomObject]@{ ShouldThrottle = ($delay -gt 0) DelaySeconds = [math]::Max(0, [math]::Round($delay, 2)) Reason = $reason } } function Update-ThrottleState { [CmdletBinding()] param ( [Parameter(Mandatory)] [pscustomobject] $State, [hashtable] $Headers, [int] $StatusCode ) if ($Headers) { foreach ($key in $Headers.Keys) { if ($key -match '^(?i)x-ms-ratelimit-remaining-') { $value = [int]$Headers[$key] $State.RemainingQuotaByHeader[$key] = $value if (($null -eq $State.RemainingQuota) -or ($value -lt $State.RemainingQuota)) { $State.RemainingQuota = $value } if (-not $State.InitialQuota) { $State.InitialQuota = $value } } } foreach ($key in $Headers.Keys) { if ($key -match '^(?i)x-ratelimit-remaining$') { $value = [int]$Headers[$key] $State.RemainingQuota = $value if (-not $State.InitialQuota) { $State.InitialQuota = $value } break } } foreach ($key in $Headers.Keys) { if ($key -match '^(?i)retry-after$') { $retryAfter = $Headers[$key] $retryUntil = Get-RetryAfterUntil -RetryAfter $retryAfter if ($retryUntil) { $State.RetryAfterUntil = $retryUntil } break } } foreach ($key in $Headers.Keys) { if ($key -match '^(?i)x-ratelimit-reset$') { $reset = $Headers[$key] $epoch = 0L if ([long]::TryParse($reset.ToString(), [ref]$epoch)) { $State.RetryAfterUntil = [DateTimeOffset]::FromUnixTimeSeconds($epoch).UtcDateTime } break } } } if ($StatusCode -in 429, 403) { $State.ConsecutiveFailures++ } elseif ($StatusCode -ge 200 -and $StatusCode -lt 300) { $State.ConsecutiveFailures = 0 } if ($State.ConsecutiveFailures -ge $State.CircuitBreakerThreshold) { $State.CircuitOpenUntil = (Get-Date).AddSeconds(60) } } function Get-RetryAfterUntil { param ( [Parameter(Mandatory)] [string] $RetryAfter ) $seconds = 0 if ([int]::TryParse($RetryAfter, [ref]$seconds)) { return (Get-Date).AddSeconds($seconds) } $parsed = [datetime]::MinValue if ([datetime]::TryParse($RetryAfter, [ref]$parsed)) { return $parsed } return $null } |